dgc-core 2.0.4 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -69,7 +69,8 @@ function saveGiveaway(raw, existing, expectedRevision) {
69
69
  (input.startsAt !== new Date(existing.startsAt).toISOString() ||
70
70
  input.closesAt !== new Date(existing.closesAt).toISOString() ||
71
71
  input.rules !== existing.rules ||
72
- JSON.stringify(input.countries) !== JSON.stringify(existing.countries) ||
72
+ JSON.stringify(input.countries) !==
73
+ JSON.stringify(existing.category === giveaway_2.GiveawayCategory.Credits ? [] : existing.countries) ||
73
74
  input.category !== existing.category ||
74
75
  input.creditAmount !== (existing.creditAmount || 0) ||
75
76
  input.claimWindowDays !== (existing.claimWindowDays || 14) ||
@@ -127,6 +128,7 @@ function transition(existing, action, revision) {
127
128
  const status = effectiveStatus(existing, clock);
128
129
  const g = {
129
130
  ...existing,
131
+ countries: existing.category === giveaway_2.GiveawayCategory.Credits ? [] : existing.countries,
130
132
  launch: { ...existing.launch },
131
133
  status,
132
134
  updatedAt: now,
@@ -177,16 +179,12 @@ function submitEntry(g, account, raw) {
177
179
  new Date(g.startsAt) > now ||
178
180
  new Date(g.closesAt) <= now)
179
181
  throw new http_error_1.HttpError('This giveaway is not accepting entries', 409);
180
- const country = input.country;
181
- if (g.countries.length && (!country || !g.countries.includes(country)))
182
- throw new http_error_1.HttpError('This country is not eligible', 422);
183
182
  const at = now.toISOString();
184
183
  return {
185
184
  giveawayId: g._id,
186
185
  userId: account.userId,
187
186
  name: [account.firstName?.trim(), account.lastName?.trim()].filter(Boolean).join(' '),
188
187
  email: account.email,
189
- country,
190
188
  _collectorType: account._collectorType,
191
189
  enteredAt: at,
192
190
  rulesAcceptedAt: at,
@@ -55,11 +55,13 @@ export declare class GiveawayRepository {
55
55
  _collectorType: UserCollectorType | null;
56
56
  name: string;
57
57
  giveawayId: import("../types").GiveawayObjectId;
58
+ country?: string | undefined;
58
59
  enteredAt: string;
59
60
  rulesSnapshot: string;
60
61
  userId: string;
61
62
  address?: import("../types").GiveawayDeliveryAddress | undefined;
62
- country?: string | undefined;
63
+ deliveryName?: string | undefined;
64
+ contactNumber?: string | undefined;
63
65
  rulesAcceptedAt: string;
64
66
  marketingConsent: boolean;
65
67
  marketingConsentAt: string;
@@ -30,6 +30,9 @@ function normalizeEntry(record) {
30
30
  delete fields.username;
31
31
  return { ...fields, name: record.name ?? legacy.fullName ?? '' };
32
32
  }
33
+ function normalizeCountries(record) {
34
+ return record.category === giveaway_2.GiveawayCategory.Credits ? { ...record, countries: [] } : record;
35
+ }
33
36
  function pagination(params) {
34
37
  const integer = (key, fallback, max) => {
35
38
  const value = Number(params.get(key));
@@ -173,7 +176,7 @@ class GiveawayRepository {
173
176
  const giveaway = await this.giveaways.findOne({ _id: objectId(id) }, { session, projection });
174
177
  if (!giveaway)
175
178
  throw new domain_1.DomainError('Giveaway not found', 404);
176
- return serialize(giveaway);
179
+ return normalizeCountries(serialize(giveaway));
177
180
  }
178
181
  async getPublicGiveaway(id) {
179
182
  const g = await this.getGiveaway(id);
@@ -224,7 +227,7 @@ class GiveawayRepository {
224
227
  totalCount: total,
225
228
  data: rows.map(row => {
226
229
  const g = row;
227
- return { ...g, status: (0, domain_1.effectiveStatus)(g) };
230
+ return { ...normalizeCountries(g), status: (0, domain_1.effectiveStatus)(g) };
228
231
  })
229
232
  };
230
233
  }
@@ -240,7 +243,13 @@ class GiveawayRepository {
240
243
  if (country && country !== 'all')
241
244
  filter.$and = [
242
245
  ...(filter.$and || []),
243
- { $or: [{ countries: country }, { countries: { $size: 0 } }] }
246
+ {
247
+ $or: [
248
+ { category: giveaway_2.GiveawayCategory.Credits },
249
+ { countries: country },
250
+ { countries: { $size: 0 } }
251
+ ]
252
+ }
244
253
  ];
245
254
  const sort = params.get('sort') || 'recent';
246
255
  const order = sort === 'closing'
@@ -259,7 +268,7 @@ class GiveawayRepository {
259
268
  this.giveaways.countDocuments(filter)
260
269
  ]);
261
270
  return {
262
- data: rows.map(g => ({ ...g, status: (0, domain_1.effectiveStatus)(g) })),
271
+ data: rows.map(g => ({ ...normalizeCountries(g), status: (0, domain_1.effectiveStatus)(g) })),
263
272
  total,
264
273
  page,
265
274
  limit
@@ -895,7 +904,7 @@ class GiveawayRepository {
895
904
  .findOne({ _id: objectId(id) }, { session });
896
905
  if (!preset)
897
906
  throw new domain_1.DomainError('Preset not found', 404);
898
- return serialize(preset);
907
+ return normalizeCountries(serialize(preset));
899
908
  }
900
909
  async listPresets() {
901
910
  return (await this.db
@@ -903,7 +912,7 @@ class GiveawayRepository {
903
912
  .find({})
904
913
  .sort({ name: 1 })
905
914
  .limit(100)
906
- .toArray()).map(serialize);
915
+ .toArray()).map(preset => normalizeCountries(serialize(preset)));
907
916
  }
908
917
  async installCreditPresets(actor) {
909
918
  const now = new Date().toISOString();
@@ -1006,7 +1015,7 @@ class GiveawayRepository {
1006
1015
  };
1007
1016
  }
1008
1017
  async getUserClaim(giveawayId, userId) {
1009
- const entry = await this.entries.findOne({ giveawayId, userId }, { projection: { _id: 1 } });
1018
+ const entry = await this.entries.findOne({ giveawayId, userId }, { projection: { _id: 1, name: 1, fullName: 1 } });
1010
1019
  if (!entry)
1011
1020
  throw new domain_1.DomainError('Winning entry not found', 404);
1012
1021
  const winner = await this.winners.findOne({ giveawayId, entryId: entry._id, replacedAt: null });
@@ -1027,7 +1036,14 @@ class GiveawayRepository {
1027
1036
  tracking: value.tracking,
1028
1037
  dispatchedAt: value.dispatchedAt,
1029
1038
  creditedAt: value.creditedAt,
1030
- claimExpired: !value.claimedAt && !!value.claimDeadline && new Date(value.claimDeadline) <= new Date()
1039
+ claimExpired: !value.claimedAt && !!value.claimDeadline && new Date(value.claimDeadline) <= new Date(),
1040
+ ...(value.category === giveaway_2.GiveawayCategory.Products
1041
+ ? {
1042
+ defaults: {
1043
+ fullName: value.claim?.fullName ?? normalizeEntry(entry).name
1044
+ }
1045
+ }
1046
+ : {})
1031
1047
  };
1032
1048
  }
1033
1049
  async claimPrize(giveawayId, userId, raw) {
@@ -1036,7 +1052,7 @@ class GiveawayRepository {
1036
1052
  const entry = await this.entries.findOne({ giveawayId, userId }, { session });
1037
1053
  const winner = entry &&
1038
1054
  (await this.winners.findOne({ giveawayId, entryId: entry._id, replacedAt: null }, { session }));
1039
- if (!winner)
1055
+ if (!entry || !winner)
1040
1056
  throw new domain_1.DomainError('Winning entry not found', 404);
1041
1057
  if (!Object.values(giveaway_2.GiveawayCategory).includes(winner.category))
1042
1058
  throw new domain_1.DomainError('Invalid giveaway category', 409);
@@ -1048,7 +1064,15 @@ class GiveawayRepository {
1048
1064
  throw new domain_1.DomainError('The claim window has closed', 409);
1049
1065
  if (winner.category === giveaway_2.GiveawayCategory.Credits)
1050
1066
  schemas_1.creditClaimSchema.parse(raw);
1051
- const claim = winner.category === giveaway_2.GiveawayCategory.Products ? schemas_1.physicalClaimSchema.parse(raw) : undefined;
1067
+ const input = winner.category === giveaway_2.GiveawayCategory.Products ? schemas_1.physicalClaimSchema.parse(raw) : undefined;
1068
+ const claim = input
1069
+ ? {
1070
+ ...input,
1071
+ fullName: schemas_1.physicalClaimSchema.shape.fullName
1072
+ .unwrap()
1073
+ .parse(input.fullName ?? normalizeEntry(entry).name)
1074
+ }
1075
+ : undefined;
1052
1076
  if (claim && g.countries.length && !g.countries.includes(claim.address.country))
1053
1077
  throw new domain_1.DomainError('This delivery country is not eligible', 422);
1054
1078
  const fields = {
@@ -1060,6 +1084,17 @@ class GiveawayRepository {
1060
1084
  ...(claim ? { claim } : {})
1061
1085
  };
1062
1086
  await this.winners.updateOne({ _id: winner._id, replacedAt: null }, { $set: fields }, { session });
1087
+ if (claim) {
1088
+ const savedEntry = await this.entries.updateOne({ _id: entry._id, giveawayId, userId }, {
1089
+ $set: {
1090
+ deliveryName: claim.fullName,
1091
+ address: claim.address,
1092
+ contactNumber: claim.contactNumber
1093
+ }
1094
+ }, { session });
1095
+ if (!savedEntry.matchedCount)
1096
+ throw new domain_1.DomainError('Winning entry changed. Please check your claim status.', 409);
1097
+ }
1063
1098
  await this.updateGiveaway({ ...g, revision: g.revision + 1 }, g.revision, session);
1064
1099
  await this.audit(userId, 'Prize claimed', giveawayId, winner._id.toHexString(), session);
1065
1100
  return { ...serialize(winner), ...fields };
@@ -1245,7 +1280,8 @@ class GiveawayRepository {
1245
1280
  });
1246
1281
  }
1247
1282
  async exportEntries(actor, id, marketingOnly) {
1248
- await this.getGiveaway(id);
1283
+ const giveaway = await this.getGiveaway(id);
1284
+ const includeDelivery = giveaway.category === giveaway_2.GiveawayCategory.Products;
1249
1285
  await this.audit(actor, marketingOnly ? 'Marketing opt-ins export started' : 'Entrants export started', id);
1250
1286
  const pipeline = [
1251
1287
  {
@@ -1270,12 +1306,18 @@ class GiveawayRepository {
1270
1306
  : [
1271
1307
  'Name',
1272
1308
  'Email',
1273
- 'Address 1',
1274
- 'Address 2',
1275
- 'City',
1276
- 'County / State',
1277
- 'Postcode / ZIP',
1278
- 'Country',
1309
+ ...(includeDelivery
1310
+ ? [
1311
+ 'Delivery name',
1312
+ 'Address 1',
1313
+ 'Address 2',
1314
+ 'City',
1315
+ 'County / State',
1316
+ 'Postcode / ZIP',
1317
+ 'Country',
1318
+ 'Contact number'
1319
+ ]
1320
+ : []),
1279
1321
  'User ID',
1280
1322
  'Collector type',
1281
1323
  'Entry time',
@@ -1291,12 +1333,18 @@ class GiveawayRepository {
1291
1333
  : [
1292
1334
  e.name,
1293
1335
  e.email,
1294
- e.address?.line1 || '',
1295
- e.address?.line2 || '',
1296
- e.address?.city || '',
1297
- e.address?.region || '',
1298
- e.address?.postalCode || '',
1299
- e.address?.country || '',
1336
+ ...(includeDelivery
1337
+ ? [
1338
+ e.deliveryName || '',
1339
+ e.address?.line1 || '',
1340
+ e.address?.line2 || '',
1341
+ e.address?.city || '',
1342
+ e.address?.region || '',
1343
+ e.address?.postalCode || '',
1344
+ e.address?.country || e.country || '',
1345
+ e.contactNumber || ''
1346
+ ]
1347
+ : []),
1300
1348
  e.userId,
1301
1349
  e._collectorType === null ? 'Unknown' : user_1.UserCollectorType[e._collectorType],
1302
1350
  e.enteredAt,
@@ -1,7 +1,7 @@
1
1
  import { GiveawayCategory, GiveawaySelectionMethod } from '../enums/giveaway';
2
2
  import { z } from 'zod';
3
3
  export declare const entryIdSchema: z.ZodString;
4
- export declare const draftSchema: z.ZodObject<{
4
+ export declare const draftSchema: z.ZodPipe<z.ZodObject<{
5
5
  images: z.ZodDefault<z.ZodArray<z.ZodString>>;
6
6
  title: z.ZodString;
7
7
  description: z.ZodString;
@@ -37,8 +37,80 @@ export declare const draftSchema: z.ZodObject<{
37
37
  message: z.ZodString;
38
38
  }, z.core.$strip>;
39
39
  }, z.core.$strip>>;
40
- }, z.core.$strip>;
41
- export declare const giveawaySchema: z.ZodObject<{
40
+ }, z.core.$strip>, z.ZodTransform<{
41
+ countries: string[];
42
+ images: string[];
43
+ title: string;
44
+ description: string;
45
+ image: string;
46
+ startsAt: string;
47
+ closesAt: string;
48
+ rules: string;
49
+ category: GiveawayCategory;
50
+ creditAmount: number;
51
+ claimWindowDays: number;
52
+ deliveryInformation: string;
53
+ prizes: {
54
+ name: string;
55
+ quantity: 1;
56
+ value: number;
57
+ _id?: string | undefined;
58
+ }[];
59
+ launch: {
60
+ enabled: boolean;
61
+ title: string;
62
+ message: string;
63
+ };
64
+ reminders: {
65
+ closing: {
66
+ enabled: boolean;
67
+ title: string;
68
+ message: string;
69
+ };
70
+ results: {
71
+ enabled: boolean;
72
+ title: string;
73
+ message: string;
74
+ };
75
+ };
76
+ }, {
77
+ images: string[];
78
+ title: string;
79
+ description: string;
80
+ image: string;
81
+ startsAt: string;
82
+ closesAt: string;
83
+ countries: string[];
84
+ rules: string;
85
+ category: GiveawayCategory;
86
+ creditAmount: number;
87
+ claimWindowDays: number;
88
+ deliveryInformation: string;
89
+ prizes: {
90
+ name: string;
91
+ quantity: 1;
92
+ value: number;
93
+ _id?: string | undefined;
94
+ }[];
95
+ launch: {
96
+ enabled: boolean;
97
+ title: string;
98
+ message: string;
99
+ };
100
+ reminders: {
101
+ closing: {
102
+ enabled: boolean;
103
+ title: string;
104
+ message: string;
105
+ };
106
+ results: {
107
+ enabled: boolean;
108
+ title: string;
109
+ message: string;
110
+ };
111
+ };
112
+ }>>;
113
+ export declare const giveawaySchema: z.ZodPipe<z.ZodObject<{
42
114
  description: z.ZodString;
43
115
  image: z.ZodString;
44
116
  rules: z.ZodString;
@@ -74,14 +146,85 @@ export declare const giveawaySchema: z.ZodObject<{
74
146
  message: z.ZodString;
75
147
  }, z.core.$strip>;
76
148
  }, z.core.$strip>>;
77
- }, z.core.$strip>;
149
+ }, z.core.$strip>, z.ZodTransform<{
150
+ countries: string[];
151
+ description: string;
152
+ image: string;
153
+ rules: string;
154
+ images: string[];
155
+ title: string;
156
+ startsAt: string;
157
+ closesAt: string;
158
+ category: GiveawayCategory;
159
+ creditAmount: number;
160
+ claimWindowDays: number;
161
+ deliveryInformation: string;
162
+ prizes: {
163
+ name: string;
164
+ quantity: 1;
165
+ value: number;
166
+ _id?: string | undefined;
167
+ }[];
168
+ launch: {
169
+ enabled: boolean;
170
+ title: string;
171
+ message: string;
172
+ };
173
+ reminders: {
174
+ closing: {
175
+ enabled: boolean;
176
+ title: string;
177
+ message: string;
178
+ };
179
+ results: {
180
+ enabled: boolean;
181
+ title: string;
182
+ message: string;
183
+ };
184
+ };
185
+ }, {
186
+ description: string;
187
+ image: string;
188
+ rules: string;
189
+ images: string[];
190
+ title: string;
191
+ startsAt: string;
192
+ closesAt: string;
193
+ countries: string[];
194
+ category: GiveawayCategory;
195
+ creditAmount: number;
196
+ claimWindowDays: number;
197
+ deliveryInformation: string;
198
+ prizes: {
199
+ name: string;
200
+ quantity: 1;
201
+ value: number;
202
+ _id?: string | undefined;
203
+ }[];
204
+ launch: {
205
+ enabled: boolean;
206
+ title: string;
207
+ message: string;
208
+ };
209
+ reminders: {
210
+ closing: {
211
+ enabled: boolean;
212
+ title: string;
213
+ message: string;
214
+ };
215
+ results: {
216
+ enabled: boolean;
217
+ title: string;
218
+ message: string;
219
+ };
220
+ };
221
+ }>>;
78
222
  export declare const entrySchema: z.ZodObject<{
79
- country: z.ZodOptional<z.ZodString>;
80
223
  rulesAccepted: z.ZodLiteral<true>;
81
224
  marketingConsent: z.ZodDefault<z.ZodBoolean>;
82
225
  }, z.core.$strip>;
83
226
  export declare const physicalClaimSchema: z.ZodObject<{
84
- fullName: z.ZodString;
227
+ fullName: z.ZodOptional<z.ZodString>;
85
228
  address: z.ZodObject<{
86
229
  line1: z.ZodString;
87
230
  line2: z.ZodDefault<z.ZodString>;
@@ -109,7 +252,7 @@ export declare const selectionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
109
252
  reason: z.ZodString;
110
253
  revision: z.ZodNumber;
111
254
  }, z.core.$strip>], "method">;
112
- export declare const presetSchema: z.ZodObject<{
255
+ export declare const presetSchema: z.ZodPipe<z.ZodObject<{
113
256
  name: z.ZodString;
114
257
  title: z.ZodString;
115
258
  description: z.ZodString;
@@ -120,4 +263,26 @@ export declare const presetSchema: z.ZodObject<{
120
263
  countries: z.ZodArray<z.ZodString>;
121
264
  rules: z.ZodString;
122
265
  deliveryInformation: z.ZodDefault<z.ZodString>;
123
- }, z.core.$strip>;
266
+ }, z.core.$strip>, z.ZodTransform<{
267
+ countries: string[];
268
+ name: string;
269
+ title: string;
270
+ description: string;
271
+ image: string;
272
+ images: string[];
273
+ category: GiveawayCategory;
274
+ creditAmount: number;
275
+ rules: string;
276
+ deliveryInformation: string;
277
+ }, {
278
+ name: string;
279
+ title: string;
280
+ description: string;
281
+ image: string;
282
+ images: string[];
283
+ category: GiveawayCategory;
284
+ creditAmount: number;
285
+ countries: string[];
286
+ rules: string;
287
+ deliveryInformation: string;
288
+ }>>;
@@ -67,7 +67,8 @@ exports.draftSchema = zod_1.z
67
67
  .refine(v => new Date(v.closesAt) > new Date(v.startsAt), {
68
68
  message: 'Closing time must be after start time',
69
69
  path: ['closesAt']
70
- });
70
+ })
71
+ .transform(v => ({ ...v, countries: v.category === giveaway_1.GiveawayCategory.Credits ? [] : v.countries }));
71
72
  exports.giveawaySchema = zod_1.z
72
73
  .object({
73
74
  ...fields,
@@ -86,14 +87,14 @@ exports.giveawaySchema = zod_1.z
86
87
  for (const [name, value] of Object.entries({ launch: v.launch, ...v.reminders }))
87
88
  if (value.enabled && (!value.title || !value.message))
88
89
  issue([name], 'Add a push title and message');
89
- });
90
+ })
91
+ .transform(v => ({ ...v, countries: v.category === giveaway_1.GiveawayCategory.Credits ? [] : v.countries }));
90
92
  exports.entrySchema = zod_1.z.object({
91
- country: country.optional(),
92
93
  rulesAccepted: zod_1.z.literal(true, { error: 'You must agree to the giveaway rules' }),
93
94
  marketingConsent: zod_1.z.boolean().default(false)
94
95
  });
95
96
  exports.physicalClaimSchema = zod_1.z.object({
96
- fullName: zod_1.z.string().trim().min(2).max(120),
97
+ fullName: zod_1.z.string().trim().min(2).max(120).optional(),
97
98
  address: zod_1.z.object({
98
99
  line1: zod_1.z.string().trim().min(2).max(200),
99
100
  line2: zod_1.z.string().trim().max(200).default(''),
@@ -139,4 +140,5 @@ exports.presetSchema = zod_1.z
139
140
  .refine(v => v.category !== giveaway_1.GiveawayCategory.Credits || v.creditAmount > 0, {
140
141
  message: 'Configure the instant credit reward amount',
141
142
  path: ['creditAmount']
142
- });
143
+ })
144
+ .transform(v => ({ ...v, countries: v.category === giveaway_1.GiveawayCategory.Credits ? [] : v.countries }));
@@ -72,8 +72,11 @@ export type GiveawayEntry = {
72
72
  userId: string;
73
73
  name: string;
74
74
  email: string;
75
- /** Historical entry addresses are retained; new entries collect delivery only at claim. */
75
+ /** Product delivery details are recorded only after the winner claims. */
76
76
  address?: GiveawayDeliveryAddress;
77
+ deliveryName?: string;
78
+ contactNumber?: string;
79
+ /** Retained only for historical entry records; new entries collect country at claim. */
77
80
  country?: string;
78
81
  /** Null only for legacy entries whose exact collector tier was not recorded. */
79
82
  _collectorType: UserCollectorType | null;
@@ -187,6 +190,11 @@ export type GiveawayPhysicalClaim = {
187
190
  address: GiveawayDeliveryAddress;
188
191
  contactNumber: string;
189
192
  };
193
+ export type GiveawayPhysicalClaimInput = {
194
+ fullName?: string;
195
+ address: Omit<GiveawayDeliveryAddress, 'line2' | 'region'> & Partial<Pick<GiveawayDeliveryAddress, 'line2' | 'region'>>;
196
+ contactNumber: string;
197
+ };
190
198
  export type GiveawayTicket = {
191
199
  entry: Pick<GiveawayEntry, '_id' | 'giveawayId' | 'enteredAt' | 'rulesSnapshot'> & {
192
200
  tickets: 1;
@@ -197,6 +205,10 @@ export type GiveawayTicket = {
197
205
  };
198
206
  export type GiveawayClaim = Pick<GiveawayWinner, '_id' | 'giveawayId' | 'entryId' | 'creditAmount' | 'category' | 'status' | 'claimDeadline' | 'claimedAt' | 'claim' | 'courier' | 'tracking' | 'dispatchedAt' | 'creditedAt'> & {
199
207
  claimExpired: boolean;
208
+ /** Product delivery name defaults to the entrant name and may be edited. */
209
+ defaults?: {
210
+ fullName: string;
211
+ };
200
212
  };
201
213
  export type GiveawayPreset = {
202
214
  _id: GiveawayObjectId;
@@ -245,7 +257,6 @@ export type GiveawayTransitionInput = {
245
257
  revision: number;
246
258
  };
247
259
  export type GiveawayEntryInput = {
248
- country?: string;
249
260
  rulesAccepted: true;
250
261
  marketingConsent?: boolean;
251
262
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dgc-core",
3
- "version": "2.0.4",
3
+ "version": "2.0.5",
4
4
  "description": "Shared DGC contracts, business rules and server utilities",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",