dgc-core 2.0.5 → 2.0.8

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.
package/README.md CHANGED
@@ -60,9 +60,15 @@ adapters stay in the application. Collection and index provisioning stays in
60
60
  `dgc-db-ops`.
61
61
 
62
62
  Giveaway documents use MongoDB ObjectIds, serialized as 24-character hexadecimal
63
- strings in APIs; entry IDs use 12 decimal characters and preserve leading zeros.
64
- Firebase `userId` identifies each entrant. Collector type is informational and
65
- never changes eligibility. Administrative entry and winner records contain
63
+ strings in APIs; new entry IDs use DGC- plus 12 uppercase letters/digits. Existing 12-digit ticket IDs remain readable.
64
+ Firebase `userId` identifies each entrant. Products have no collector-tier restriction for authenticated
65
+ accounts, including accounts with a missing or unsupported tier. Credits allow only Rookie (1),
66
+ Collector (3) and Elite (0); Free (2), non-members and missing or unsupported tiers cannot enter credits.
67
+ Public `eligibleCollectorTypes` is `null` for unrestricted Products and `[1, 3, 0]` for Credits.
68
+ Entry snapshots preserve known numeric tiers and use `null` for missing or unsupported values.
69
+ Eligible entries have equal chances; credit winner selection uses the saved entry tier while product
70
+ draws include all valid entries. Push-token audience filters belong to the delivery adapter and do not
71
+ restrict entry submission. Administrative entry and winner records contain
66
72
  personal information and require authorized access; use `GiveawayPublic` for
67
73
  public app responses. `DomainError` and `HttpError` refer to the same constructor.
68
74
 
@@ -83,3 +89,11 @@ send notifications or connect to a live database.
83
89
  ## Giveaway MVP v2
84
90
 
85
91
  One ticket per Firebase account, one winner per giveaway, saved random/manual selection and explicit confirmation. Physical address and contact number are collected at claim only. Credit rewards snapshot `creditAmount` and use `fulfillGiveawayCredits` to atomically change Firebase `instantCredits` plus a permanent per-giveaway marker. Status reads reconcile without issuing credits. See dgc-giveaways/docs/api.md and integration.md for contracts and release order.
92
+
93
+ ## RevenueCat entitlement verification
94
+
95
+ The shared RevenueCat catalog maps Rookie, Collector and Elite entitlement IDs for project `projb48c5a30`. It contains no API key. The browser-safe catalog and evidence types are available from `dgc-core/types`; the HTTP client is exported only from `dgc-core`.
96
+
97
+ `createRevenueCatClient` accepts server-provided credentials and request limits. `verifyCreditGiveawayEntitlement` checks all eligible tiers using one paginated customer lookup. Null expiry represents non-expiring access; expired entitlements are excluded. Network, rate-limit and malformed-response failures throw a sanitized 503 without exposing credentials. The active entitlements endpoint has no environment filter; configure RevenueCat Sandbox Testing Access and test-account policy for the selected project.
98
+
99
+ Credit winner selection, confirmation and replacement require an injected `checkCreditEligibility(userId)` callback in the optional third `GiveawayRepository` constructor argument. Missing verification is a 503, never a fallback to saved membership. Product operations do not call the verifier. Checks occur outside transactions; reviewed revisions, candidate identity and unexpired evidence are validated at commit. Random selection picks one candidate first, then checks their entitlement. Inactive candidates are retried without replacement up to `creditWinnerMaxAttempts` (default 5; integer 1-100). It never checks the whole entrant pool in advance. The 30-second draw budget remains in place. Transient RevenueCat network/429/5xx failures retry the same user and page up to three attempts with bounded backoff and Retry-After handling, sharing the existing total lookup deadline. Authorization, missing-resource and malformed-response failures are not retried or treated as inactive access. Evidence is stored on selections and winners. Existing confirmed claims and idempotent rewards do not recheck access.
@@ -0,0 +1,7 @@
1
+ import type { RevenueCatEntitlementIds } from '../types/revenuecat';
2
+ export declare const REVENUECAT_API_V2 = "https://api.revenuecat.com/v2";
3
+ export declare const DGC_REVENUECAT_PROJECT_ID = "projb48c5a30";
4
+ export declare const DGC_REVENUECAT_ELITE_ENTITLEMENT_ID = "entlb1b4b2ae6b";
5
+ export declare const DGC_REVENUECAT_ENTITLEMENT_IDS: RevenueCatEntitlementIds;
6
+ /** Non-secret catalog IDs are shared; callers can supply a separate test-project catalog. */
7
+ export declare function createRevenueCatEntitlementIds(values?: Partial<RevenueCatEntitlementIds>): RevenueCatEntitlementIds;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DGC_REVENUECAT_ENTITLEMENT_IDS = exports.DGC_REVENUECAT_ELITE_ENTITLEMENT_ID = exports.DGC_REVENUECAT_PROJECT_ID = exports.REVENUECAT_API_V2 = void 0;
4
+ exports.createRevenueCatEntitlementIds = createRevenueCatEntitlementIds;
5
+ exports.REVENUECAT_API_V2 = 'https://api.revenuecat.com/v2';
6
+ exports.DGC_REVENUECAT_PROJECT_ID = 'projb48c5a30';
7
+ exports.DGC_REVENUECAT_ELITE_ENTITLEMENT_ID = 'entlb1b4b2ae6b';
8
+ exports.DGC_REVENUECAT_ENTITLEMENT_IDS = Object.freeze({
9
+ rookie: 'entl74fb3d3bc3',
10
+ collector: 'entl0bea318713',
11
+ elite: exports.DGC_REVENUECAT_ELITE_ENTITLEMENT_ID
12
+ });
13
+ /** Non-secret catalog IDs are shared; callers can supply a separate test-project catalog. */
14
+ function createRevenueCatEntitlementIds(values = {}) {
15
+ const ids = {
16
+ rookie: (values.rookie ?? exports.DGC_REVENUECAT_ENTITLEMENT_IDS.rookie).trim(),
17
+ collector: (values.collector ?? exports.DGC_REVENUECAT_ENTITLEMENT_IDS.collector).trim(),
18
+ elite: (values.elite ?? exports.DGC_REVENUECAT_ENTITLEMENT_IDS.elite).trim()
19
+ };
20
+ if (Object.values(ids).some(id => typeof id !== 'string' || !/^entl[a-zA-Z0-9]+$/.test(id)))
21
+ throw new Error('Configure valid RevenueCat entitlement IDs for Rookie, Collector and Elite');
22
+ if (new Set(Object.values(ids)).size !== 3)
23
+ throw new Error('RevenueCat tier entitlement IDs must be distinct');
24
+ return Object.freeze(ids);
25
+ }
@@ -39,11 +39,13 @@ export declare enum GiveawayNotificationStatus {
39
39
  Pending = "pending",
40
40
  Delivering = "delivering",
41
41
  Sent = "sent",
42
- Failed = "failed"
42
+ Failed = "failed",
43
+ Skipped = "skipped"
43
44
  }
44
45
  export declare enum GiveawayNotificationDeliveryStatus {
45
46
  Pending = "pending",
46
- Queued = "queued"
47
+ Queued = "queued",
48
+ Skipped = "skipped"
47
49
  }
48
50
  export declare enum GiveawayTicketResult {
49
51
  Pending = "pending",
@@ -50,11 +50,13 @@ var GiveawayNotificationStatus;
50
50
  GiveawayNotificationStatus["Delivering"] = "delivering";
51
51
  GiveawayNotificationStatus["Sent"] = "sent";
52
52
  GiveawayNotificationStatus["Failed"] = "failed";
53
+ GiveawayNotificationStatus["Skipped"] = "skipped";
53
54
  })(GiveawayNotificationStatus || (exports.GiveawayNotificationStatus = GiveawayNotificationStatus = {}));
54
55
  var GiveawayNotificationDeliveryStatus;
55
56
  (function (GiveawayNotificationDeliveryStatus) {
56
57
  GiveawayNotificationDeliveryStatus["Pending"] = "pending";
57
58
  GiveawayNotificationDeliveryStatus["Queued"] = "queued";
59
+ GiveawayNotificationDeliveryStatus["Skipped"] = "skipped";
58
60
  })(GiveawayNotificationDeliveryStatus || (exports.GiveawayNotificationDeliveryStatus = GiveawayNotificationDeliveryStatus = {}));
59
61
  var GiveawayTicketResult;
60
62
  (function (GiveawayTicketResult) {
@@ -56,8 +56,8 @@ export declare function giveawayChanges(existing: Giveaway, updated: Giveaway):
56
56
  };
57
57
  export declare function transition(existing: Giveaway, action: GiveawayAction, revision: number): Giveaway;
58
58
  export declare function submitEntry(g: Giveaway, account: UserProfile, raw: unknown): Omit<Entry, '_id'>;
59
- export declare function confirmWinners(g: Giveaway, entries: Pick<Entry, '_id' | 'userId' | 'giveawayId'>[], alreadyConfirmed: boolean, actor: string, raw: unknown): Omit<Winner, '_id'>[];
60
- export declare function replaceWinner(g: Giveaway, old: Winner, entry: Pick<Entry, '_id' | 'giveawayId'>, alreadyWinner: boolean, actor: string, reason: string): {
59
+ export declare function confirmWinners(g: Giveaway, entries: Pick<Entry, '_id' | 'userId' | 'giveawayId' | '_collectorType'>[], alreadyConfirmed: boolean, actor: string, raw: unknown): Omit<Winner, '_id'>[];
60
+ export declare function replaceWinner(g: Giveaway, old: Winner, entry: Pick<Entry, '_id' | 'giveawayId' | '_collectorType'>, alreadyWinner: boolean, actor: string, reason: string): {
61
61
  previous: Winner;
62
62
  replacement: Omit<Winner, '_id'>;
63
63
  };
@@ -17,6 +17,8 @@ const node_util_1 = require("node:util");
17
17
  const schemas_1 = require("./schemas");
18
18
  const http_error_1 = require("../http-error");
19
19
  Object.defineProperty(exports, "DomainError", { enumerable: true, get: function () { return http_error_1.HttpError; } });
20
+ const eligibility_1 = require("./eligibility");
21
+ const user_1 = require("../enums/user");
20
22
  function effectiveStatus(g, clock = new Date()) {
21
23
  if (g.status === giveaway_2.GiveawayStatus.Scheduled) {
22
24
  if (new Date(g.closesAt) <= clock)
@@ -37,6 +39,8 @@ function launchJob(g, now = new Date().toISOString()) {
37
39
  kind: giveaway_2.GiveawayNotificationKind.Launch,
38
40
  eventKey: giveaway_2.GiveawayNotificationKind.Launch,
39
41
  audience: giveaway_2.GiveawayNotificationAudience.All,
42
+ category: g.category,
43
+ expiresAt: g.closesAt,
40
44
  title: g.launch.title,
41
45
  message: g.launch.message,
42
46
  status: giveaway_2.GiveawayNotificationStatus.Pending,
@@ -179,13 +183,18 @@ function submitEntry(g, account, raw) {
179
183
  new Date(g.startsAt) > now ||
180
184
  new Date(g.closesAt) <= now)
181
185
  throw new http_error_1.HttpError('This giveaway is not accepting entries', 409);
186
+ if (!(0, eligibility_1.isGiveawayMemberEligible)(g.category, account._collectorType))
187
+ throw new http_error_1.HttpError('Your membership is not eligible for this giveaway', 403);
182
188
  const at = now.toISOString();
183
189
  return {
184
190
  giveawayId: g._id,
185
191
  userId: account.userId,
186
192
  name: [account.firstName?.trim(), account.lastName?.trim()].filter(Boolean).join(' '),
187
193
  email: account.email,
188
- _collectorType: account._collectorType,
194
+ _collectorType: typeof account._collectorType === 'number' &&
195
+ Object.values(user_1.UserCollectorType).includes(account._collectorType)
196
+ ? account._collectorType
197
+ : null,
189
198
  enteredAt: at,
190
199
  rulesAcceptedAt: at,
191
200
  rulesSnapshot: g.rules,
@@ -211,6 +220,8 @@ function confirmWinners(g, entries, alreadyConfirmed, actor, raw) {
211
220
  const prize = g.prizes.find(prize => prize._id === assignment.prizeId);
212
221
  if (!entry || entry.giveawayId !== g._id || !prize)
213
222
  throw new http_error_1.HttpError('Invalid entrant or prize');
223
+ if (!(0, eligibility_1.isGiveawayMemberEligible)(g.category, entry._collectorType))
224
+ throw new http_error_1.HttpError('This entry is not eligible for the giveaway prize', 403);
214
225
  if (accounts.has(entry.userId))
215
226
  throw new http_error_1.HttpError('Each account can win only one prize');
216
227
  accounts.add(entry.userId);
@@ -241,6 +252,8 @@ function replaceWinner(g, old, entry, alreadyWinner, actor, reason) {
241
252
  throw new http_error_1.HttpError('Winner replacements require a closed giveaway');
242
253
  if (old.giveawayId !== g._id || old.replacedAt || entry.giveawayId !== g._id)
243
254
  throw new http_error_1.HttpError('Winner or entrant not found', 404);
255
+ if (!(0, eligibility_1.isGiveawayMemberEligible)(g.category, entry._collectorType))
256
+ throw new http_error_1.HttpError('This entry is not eligible for the giveaway prize', 403);
244
257
  if (reason.trim().length < 5)
245
258
  throw new http_error_1.HttpError('Provide a replacement reason (at least 5 characters)');
246
259
  if (old.status !== giveaway_2.GiveawayWinnerStatus.AwaitingClaim || old.claimedAt)
@@ -0,0 +1,5 @@
1
+ import { GiveawayCategory } from '../enums/giveaway';
2
+ import { UserCollectorType } from '../enums/user';
3
+ /** Null means entry and winner eligibility has no collector-tier restriction. */
4
+ export declare function giveawayEligibleCollectorTypes(category: GiveawayCategory): readonly UserCollectorType[] | null;
5
+ export declare function isGiveawayMemberEligible(category: GiveawayCategory, collectorType: unknown): boolean;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.giveawayEligibleCollectorTypes = giveawayEligibleCollectorTypes;
4
+ exports.isGiveawayMemberEligible = isGiveawayMemberEligible;
5
+ const giveaway_1 = require("../enums/giveaway");
6
+ const user_1 = require("../enums/user");
7
+ const creditMembers = Object.freeze([
8
+ user_1.UserCollectorType.Rookie,
9
+ user_1.UserCollectorType.Collector,
10
+ user_1.UserCollectorType.Elite
11
+ ]);
12
+ const noMembers = Object.freeze([]);
13
+ /** Null means entry and winner eligibility has no collector-tier restriction. */
14
+ function giveawayEligibleCollectorTypes(category) {
15
+ if (category === giveaway_1.GiveawayCategory.Products)
16
+ return null;
17
+ if (category === giveaway_1.GiveawayCategory.Credits)
18
+ return creditMembers;
19
+ return noMembers;
20
+ }
21
+ function isGiveawayMemberEligible(category, collectorType) {
22
+ const tiers = giveawayEligibleCollectorTypes(category);
23
+ return tiers === null || (typeof collectorType === 'number' && tiers.includes(collectorType));
24
+ }
@@ -2,7 +2,12 @@ import { GiveawayAction, GiveawayCategory, GiveawaySelectionMethod, GiveawayStat
2
2
  import { UserCollectorType } from '../enums/user';
3
3
  import type { ClientSession, Db, MongoClient } from 'mongodb';
4
4
  import type { GiveawayPreset, GiveawayTicket, GiveawayClaim, PaginatedApiResponse } from '../types';
5
+ import type { GiveawayCreditEligibilityEvidence } from '../types/revenuecat';
5
6
  import type { UserProfile, AuditEvent, DashboardData, Entry, EntryRecord, Giveaway, GiveawayDetailData, GiveawaySummary, NotificationJob, NotificationRecord, OverviewData, PageResult, Winner, WinnerRecord } from './types';
7
+ export type GiveawayRepositoryOptions = {
8
+ checkCreditEligibility?: (userId: string) => Promise<GiveawayCreditEligibilityEvidence | null>;
9
+ creditWinnerMaxAttempts?: number;
10
+ };
6
11
  type Stored<T extends {
7
12
  _id: string;
8
13
  }> = Omit<T, '_id'>;
@@ -15,13 +20,18 @@ export declare function pagination(params: URLSearchParams): {
15
20
  export declare class GiveawayRepository {
16
21
  private readonly db;
17
22
  private readonly client;
18
- constructor(db: Db, client: MongoClient);
23
+ private readonly options;
24
+ constructor(db: Db, client: MongoClient, options?: GiveawayRepositoryOptions);
19
25
  private get giveaways();
20
26
  private get entries();
21
27
  private get winners();
22
28
  private get notifications();
23
29
  private get marketing();
24
30
  private get activity();
31
+ private checkCreditCandidate;
32
+ private assertFreshCreditEvidence;
33
+ private assertVerifiedCreditCandidate;
34
+ private verifiedCreditSelection;
25
35
  private transaction;
26
36
  private audit;
27
37
  getGiveaway(id: string, session?: ClientSession): Promise<Giveaway>;
@@ -107,11 +117,11 @@ export declare class GiveawayRepository {
107
117
  confirmWinners(actor: string, id: string, raw: unknown): Promise<{
108
118
  _id: string;
109
119
  giveawayId: import("../types").GiveawayObjectId;
120
+ category: GiveawayCategory;
110
121
  status: GiveawayWinnerStatus;
111
122
  claimedAt?: string | undefined;
112
123
  userId?: string | undefined;
113
124
  creditAmount?: number | undefined;
114
- category: GiveawayCategory;
115
125
  selection?: import("../types").GiveawaySelection | undefined;
116
126
  entryId: import("../types").GiveawayEntryId;
117
127
  claimDeadline?: string | undefined;
@@ -123,6 +133,7 @@ export declare class GiveawayRepository {
123
133
  prizeId: import("../types").GiveawayObjectId;
124
134
  confirmedBy: string;
125
135
  confirmedAt: string;
136
+ creditEligibility?: GiveawayCreditEligibilityEvidence | undefined;
126
137
  rewardError?: string | undefined;
127
138
  replacedAt?: string | null | undefined;
128
139
  replacementReason?: string | undefined;
@@ -131,11 +142,11 @@ export declare class GiveawayRepository {
131
142
  replaceWinner(actor: string, id: string, winnerId: string, replacementEntryId: string, reason: string): Promise<{
132
143
  _id: string;
133
144
  giveawayId: import("../types").GiveawayObjectId;
145
+ category: GiveawayCategory;
134
146
  status: GiveawayWinnerStatus;
135
147
  claimedAt?: string | undefined;
136
148
  userId?: string | undefined;
137
149
  creditAmount?: number | undefined;
138
- category: GiveawayCategory;
139
150
  selection?: import("../types").GiveawaySelection | undefined;
140
151
  entryId: import("../types").GiveawayEntryId;
141
152
  claimDeadline?: string | undefined;
@@ -147,6 +158,7 @@ export declare class GiveawayRepository {
147
158
  prizeId: import("../types").GiveawayObjectId;
148
159
  confirmedBy: string;
149
160
  confirmedAt: string;
161
+ creditEligibility?: GiveawayCreditEligibilityEvidence | undefined;
150
162
  rewardError?: string | undefined;
151
163
  replacedAt?: string | null | undefined;
152
164
  replacementReason?: string | undefined;
@@ -157,10 +169,10 @@ export declare class GiveawayRepository {
157
169
  courier: string;
158
170
  tracking: string;
159
171
  giveawayId: import("../types").GiveawayObjectId;
172
+ category: GiveawayCategory;
160
173
  claimedAt?: string | undefined;
161
174
  userId?: string | undefined;
162
175
  creditAmount?: number | undefined;
163
- category: GiveawayCategory;
164
176
  selection?: import("../types").GiveawaySelection | undefined;
165
177
  entryId: import("../types").GiveawayEntryId;
166
178
  claimDeadline?: string | undefined;
@@ -170,6 +182,7 @@ export declare class GiveawayRepository {
170
182
  prizeId: import("../types").GiveawayObjectId;
171
183
  confirmedBy: string;
172
184
  confirmedAt: string;
185
+ creditEligibility?: GiveawayCreditEligibilityEvidence | undefined;
173
186
  rewardError?: string | undefined;
174
187
  replacedAt?: string | null | undefined;
175
188
  replacementReason?: string | undefined;
@@ -180,6 +193,7 @@ export declare class GiveawayRepository {
180
193
  selectWinner(actor: string, id: string, raw: unknown): Promise<{
181
194
  giveaway: {
182
195
  selection: {
196
+ creditEligibility?: GiveawayCreditEligibilityEvidence | undefined;
183
197
  entryId: string;
184
198
  method: GiveawaySelectionMethod;
185
199
  reason: string;
@@ -243,9 +257,9 @@ export declare class GiveawayRepository {
243
257
  userId: string;
244
258
  creditAmount: number;
245
259
  giveawayId: import("../types").GiveawayObjectId;
260
+ category: GiveawayCategory;
246
261
  status: GiveawayWinnerStatus;
247
262
  claimedAt?: string | undefined;
248
- category: GiveawayCategory;
249
263
  selection?: import("../types").GiveawaySelection | undefined;
250
264
  entryId: import("../types").GiveawayEntryId;
251
265
  claimDeadline?: string | undefined;
@@ -257,6 +271,7 @@ export declare class GiveawayRepository {
257
271
  prizeId: import("../types").GiveawayObjectId;
258
272
  confirmedBy: string;
259
273
  confirmedAt: string;
274
+ creditEligibility?: GiveawayCreditEligibilityEvidence | undefined;
260
275
  rewardError?: string | undefined;
261
276
  replacedAt?: string | null | undefined;
262
277
  replacementReason?: string | undefined;