From 83a10609831ee335ce84157659f4a6609f82211a Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Tue, 21 Nov 2023 15:02:15 +0700 Subject: [PATCH] Added last redeemed property to Offers (#19066) refs https://github.com/TryGhost/Product/issues/4153 - wired up a new last_redeemed prop to the Offers API endpoint. --- .../offers/OfferBookshelfRepository.js | 9 +++- .../admin/__snapshots__/offers.test.js.snap | 52 +++++++++++++------ ghost/offers/lib/application/OfferMapper.js | 6 ++- ghost/offers/lib/domain/models/Offer.js | 10 +++- 4 files changed, 58 insertions(+), 19 deletions(-) diff --git a/ghost/core/core/server/services/offers/OfferBookshelfRepository.js b/ghost/core/core/server/services/offers/OfferBookshelfRepository.js index 862e9367ad..3d66e17934 100644 --- a/ghost/core/core/server/services/offers/OfferBookshelfRepository.js +++ b/ghost/core/core/server/services/offers/OfferBookshelfRepository.js @@ -100,6 +100,12 @@ class OfferBookshelfRepository { const count = await this.OfferRedemptionModel.where({offer_id: json.id}).count('id', { transacting: options.transacting }); + + const lastRedeemed = await this.OfferRedemptionModel.where({offer_id: json.id}).orderBy('created_at', 'DESC').fetchAll({ + transacting: options.transacting, + limit: 1 + }); + try { return await Offer.create({ id: json.id, @@ -119,7 +125,8 @@ class OfferBookshelfRepository { id: json.product.id, name: json.product.name }, - created_at: json.created_at + created_at: json.created_at, + last_redeemed: lastRedeemed.toJSON().length > 0 ? lastRedeemed.toJSON()[0].created_at : null }, null); } catch (err) { logger.error(err); diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/offers.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/offers.test.js.snap index 8f9f2ba912..8d317d0778 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/offers.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/offers.test.js.snap @@ -14,6 +14,7 @@ Object { "duration": "once", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Fourth of July Sales", "redemption_count": 0, "status": "active", @@ -30,7 +31,7 @@ exports[`Offers API Can add a fixed offer 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "347", + "content-length": "368", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -55,6 +56,7 @@ Object { "duration": "once", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Black Friday", "redemption_count": 0, "status": "active", @@ -71,7 +73,7 @@ exports[`Offers API Can add a new offer 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "389", + "content-length": "410", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -96,6 +98,7 @@ Object { "duration": "once", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Easter Sales", "redemption_count": 0, "status": "active", @@ -112,7 +115,7 @@ exports[`Offers API Can add a new offer with minimal fields 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "344", + "content-length": "365", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -137,6 +140,7 @@ Object { "duration": "trial", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Fourth of July Sales trial", "redemption_count": 0, "status": "active", @@ -153,7 +157,7 @@ exports[`Offers API Can add a trial offer 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "359", + "content-length": "380", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -179,6 +183,7 @@ Object { "duration": "once", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Cyber Monday", "redemption_count": 0, "status": "archived", @@ -196,7 +201,7 @@ exports[`Offers API Can archive an offer 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "468", + "content-length": "489", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -221,6 +226,7 @@ Object { "duration": "once", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Black Friday", "redemption_count": 0, "status": "active", @@ -242,6 +248,7 @@ Object { "duration": "once", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Easter Sales", "redemption_count": 0, "status": "active", @@ -263,6 +270,7 @@ Object { "duration": "once", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Summer Sale", "redemption_count": 0, "status": "active", @@ -284,6 +292,7 @@ Object { "duration": "once", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Fourth of July Sales", "redemption_count": 0, "status": "active", @@ -305,6 +314,7 @@ Object { "duration": "trial", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Fourth of July Sales trial", "redemption_count": 0, "status": "active", @@ -322,7 +332,7 @@ exports[`Offers API Can browse 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2063", + "content-length": "2168", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -346,6 +356,7 @@ Object { "duration": "once", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Easter Sales", "redemption_count": 0, "status": "active", @@ -367,6 +378,7 @@ Object { "duration": "once", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Summer Sale", "redemption_count": 0, "status": "active", @@ -388,6 +400,7 @@ Object { "duration": "once", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Fourth of July Sales", "redemption_count": 0, "status": "active", @@ -409,6 +422,7 @@ Object { "duration": "trial", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Fourth of July Sales trial", "redemption_count": 0, "status": "active", @@ -426,7 +440,7 @@ exports[`Offers API Can browse active 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1621", + "content-length": "1705", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -450,6 +464,7 @@ Object { "duration": "once", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Cyber Monday", "redemption_count": 0, "status": "archived", @@ -467,7 +482,7 @@ exports[`Offers API Can browse archived 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "468", + "content-length": "489", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -491,6 +506,7 @@ Object { "duration": "once", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Cyber Monday", "redemption_count": 0, "status": "active", @@ -508,7 +524,7 @@ exports[`Offers API Can edit an offer 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "466", + "content-length": "487", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -533,6 +549,7 @@ Object { "duration": "once", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Black Friday", "redemption_count": 0, "status": "active", @@ -550,7 +567,7 @@ exports[`Offers API Can get a single offer 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "454", + "content-length": "475", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -574,6 +591,7 @@ Object { "duration": "trial", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Fourth of July Sales trial", "redemption_count": 0, "status": "active", @@ -591,7 +609,7 @@ exports[`Offers API Can get a trial offer 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "424", + "content-length": "445", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -708,6 +726,7 @@ Object { "duration": "once", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Cyber Monday", "redemption_count": 0, "status": "archived", @@ -725,7 +744,7 @@ exports[`Offers API Cannot update offer amount 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "468", + "content-length": "489", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -750,6 +769,7 @@ Object { "duration": "once", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Cyber Monday", "redemption_count": 0, "status": "archived", @@ -767,7 +787,7 @@ exports[`Offers API Cannot update offer cadence 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "468", + "content-length": "489", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -885,6 +905,7 @@ Object { "duration": "once", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Cyber Monday", "redemption_count": 0, "status": "archived", @@ -902,7 +923,7 @@ exports[`Offers API Cannot update offer tier 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "468", + "content-length": "489", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -945,6 +966,7 @@ Object { "duration": "once", "duration_in_months": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "last_redeemed": null, "name": "Summer Sale", "redemption_count": 0, "status": "active", @@ -961,7 +983,7 @@ exports[`Offers API Slugifies offer codes 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "347", + "content-length": "368", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/offers/lib/application/OfferMapper.js b/ghost/offers/lib/application/OfferMapper.js index bfb7d88b29..20ffb1fc10 100644 --- a/ghost/offers/lib/application/OfferMapper.js +++ b/ghost/offers/lib/application/OfferMapper.js @@ -28,7 +28,8 @@ * @prop {object} tier * @prop {string} tier.id * @prop {string} tier.name - * @prop {Date} created_at + * @prop {string} created_at + * @prop {string|null} last_redeemed */ class OfferMapper { @@ -56,7 +57,8 @@ class OfferMapper { id: offer.tier.id, name: offer.tier.name }, - created_at: offer.createdAt + created_at: offer.createdAt, + last_redeemed: offer.lastRedeemed }; } } diff --git a/ghost/offers/lib/domain/models/Offer.js b/ghost/offers/lib/domain/models/Offer.js index 749d0ae037..a9872678d6 100644 --- a/ghost/offers/lib/domain/models/Offer.js +++ b/ghost/offers/lib/domain/models/Offer.js @@ -31,6 +31,7 @@ const OfferCreatedAt = require('./OfferCreatedAt'); * @prop {OfferTier} tier * @prop {number} redemptionCount * @prop {string} createdAt + * @prop {string|null} lastRedeemed */ /** @@ -50,6 +51,7 @@ const OfferCreatedAt = require('./OfferCreatedAt'); * @prop {number} redemptionCount * @prop {TierProps|OfferTier} tier * @prop {Date} created_at + * @prop {Date|null} last_redeemed */ /** @@ -186,6 +188,10 @@ class Offer { return this.props.createdAt; } + get lastRedeemed() { + return this.props.lastRedeemed; + } + /** * @param {OfferCode} code * @param {UniqueChecker} uniqueChecker @@ -283,6 +289,7 @@ class Offer { const duration = OfferDuration.create(data.duration, data.duration_in_months); const status = OfferStatus.create(data.status || 'active'); const createdAt = isNew ? new Date().toISOString : OfferCreatedAt.create(data.created_at); + const lastRedeemed = data.last_redeemed ? new Date(data.last_redeemed).toISOString() : null; if (isNew && data.redemptionCount !== undefined) { // TODO correct error @@ -349,7 +356,8 @@ class Offer { tier, redemptionCount, status, - createdAt + createdAt, + lastRedeemed }, {isNew}); } }