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 +17 -3
- package/dist/constants/revenuecat.d.ts +7 -0
- package/dist/constants/revenuecat.js +25 -0
- package/dist/enums/giveaway.d.ts +4 -2
- package/dist/enums/giveaway.js +2 -0
- package/dist/giveaways/domain.d.ts +2 -2
- package/dist/giveaways/domain.js +14 -1
- package/dist/giveaways/eligibility.d.ts +5 -0
- package/dist/giveaways/eligibility.js +24 -0
- package/dist/giveaways/giveaway-repository.d.ts +20 -5
- package/dist/giveaways/giveaway-repository.js +253 -17
- package/dist/giveaways/index.d.ts +1 -0
- package/dist/giveaways/index.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/pub.js +3 -2
- package/dist/revenuecat.d.ts +16 -0
- package/dist/revenuecat.js +206 -0
- package/dist/task.js +10 -3
- package/dist/types/giveaway.d.ts +22 -2
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/types/revenuecat.d.ts +19 -0
- package/dist/types/revenuecat.js +2 -0
- package/package.json +1 -1
- package/dist/tw/utils.d.ts +0 -1
- package/dist/tw/utils.js +0 -19
- package/dist/tw/wallet.d.ts +0 -1
- package/dist/tw/wallet.js +0 -57
|
@@ -9,6 +9,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
9
9
|
const mongodb_1 = require("mongodb");
|
|
10
10
|
const entry_id_1 = require("./entry-id");
|
|
11
11
|
const api_pagination_1 = require("./api-pagination");
|
|
12
|
+
const eligibility_1 = require("./eligibility");
|
|
12
13
|
const domain_1 = require("./domain");
|
|
13
14
|
const schemas_1 = require("./schemas");
|
|
14
15
|
const projection = { entryVersion: 0 };
|
|
@@ -129,9 +130,10 @@ function filterCategory(filter, params) {
|
|
|
129
130
|
}
|
|
130
131
|
/** Each method reads only its own records. Transactions contain related writes only. */
|
|
131
132
|
class GiveawayRepository {
|
|
132
|
-
constructor(db, client) {
|
|
133
|
+
constructor(db, client, options = {}) {
|
|
133
134
|
this.db = db;
|
|
134
135
|
this.client = client;
|
|
136
|
+
this.options = options;
|
|
135
137
|
}
|
|
136
138
|
get giveaways() {
|
|
137
139
|
return this.db.collection('giveaways');
|
|
@@ -151,6 +153,111 @@ class GiveawayRepository {
|
|
|
151
153
|
get activity() {
|
|
152
154
|
return this.db.collection('audit');
|
|
153
155
|
}
|
|
156
|
+
async checkCreditCandidate(entry) {
|
|
157
|
+
const check = this.options.checkCreditEligibility;
|
|
158
|
+
if (!check)
|
|
159
|
+
throw new domain_1.DomainError('RevenueCat entitlement verification is not configured', 503);
|
|
160
|
+
if (typeof entry.userId !== 'string' ||
|
|
161
|
+
!entry.userId ||
|
|
162
|
+
entry.userId.length > 128 ||
|
|
163
|
+
/[.#$[\]/]/.test(entry.userId))
|
|
164
|
+
throw new domain_1.DomainError('The entrant has no valid DGC account identity', 503);
|
|
165
|
+
let evidence;
|
|
166
|
+
try {
|
|
167
|
+
evidence = await check(entry.userId);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
throw new domain_1.DomainError('Unable to verify active membership. Try again.', 503);
|
|
171
|
+
}
|
|
172
|
+
if (evidence === null)
|
|
173
|
+
return null;
|
|
174
|
+
if (!evidence ||
|
|
175
|
+
evidence.userId !== entry.userId ||
|
|
176
|
+
typeof evidence.entitlementId !== 'string' ||
|
|
177
|
+
!evidence.entitlementId.trim() ||
|
|
178
|
+
evidence.entitlementId.length > 255 ||
|
|
179
|
+
!(0, eligibility_1.isGiveawayMemberEligible)(giveaway_2.GiveawayCategory.Credits, evidence.collectorType) ||
|
|
180
|
+
typeof evidence.checkedAt !== 'string' ||
|
|
181
|
+
!Number.isFinite(Date.parse(evidence.checkedAt)) ||
|
|
182
|
+
Date.parse(evidence.checkedAt) > Date.now() + 5000 ||
|
|
183
|
+
(evidence.expiresAt !== null &&
|
|
184
|
+
(typeof evidence.expiresAt !== 'number' || !Number.isFinite(evidence.expiresAt))))
|
|
185
|
+
throw new domain_1.DomainError('Unable to verify active membership. Try again.', 503);
|
|
186
|
+
this.assertFreshCreditEvidence(evidence);
|
|
187
|
+
return {
|
|
188
|
+
userId: evidence.userId,
|
|
189
|
+
checkedAt: evidence.checkedAt,
|
|
190
|
+
entitlementId: evidence.entitlementId,
|
|
191
|
+
collectorType: evidence.collectorType,
|
|
192
|
+
expiresAt: evidence.expiresAt
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
assertFreshCreditEvidence(evidence) {
|
|
196
|
+
if (Date.now() - Date.parse(evidence.checkedAt) > 60000 ||
|
|
197
|
+
(evidence.expiresAt !== null && evidence.expiresAt <= Date.now()))
|
|
198
|
+
throw new domain_1.DomainError('Entitlement verification expired. Review and try again.', 409);
|
|
199
|
+
}
|
|
200
|
+
assertVerifiedCreditCandidate(g, reviewed, entry, verified) {
|
|
201
|
+
if (g.category !== reviewed.category || g.revision !== reviewed.revision)
|
|
202
|
+
throw new domain_1.DomainError('Giveaway changed. Review the selection again.', 409);
|
|
203
|
+
if (g.category !== giveaway_2.GiveawayCategory.Credits)
|
|
204
|
+
return;
|
|
205
|
+
if (!verified || verified.entryId !== entry._id || verified.evidence.userId !== entry.userId)
|
|
206
|
+
throw new domain_1.DomainError('Active membership must be verified before awarding credits', 503);
|
|
207
|
+
this.assertFreshCreditEvidence(verified.evidence);
|
|
208
|
+
}
|
|
209
|
+
async verifiedCreditSelection(g, input) {
|
|
210
|
+
if ((0, domain_1.effectiveStatus)(g) !== giveaway_2.GiveawayStatus.Closed)
|
|
211
|
+
throw new domain_1.DomainError('Close entries before selecting a winner', 409);
|
|
212
|
+
if (g.revision !== input.revision)
|
|
213
|
+
throw new domain_1.DomainError('Giveaway changed. Refresh before selecting.', 409);
|
|
214
|
+
if (g.selection ||
|
|
215
|
+
(await this.winners.findOne({ giveawayId: g._id, replacedAt: null }, { projection: { _id: 1 } })))
|
|
216
|
+
throw new domain_1.DomainError('A winner is already selected. Explicitly reset the selection to change it.', 409);
|
|
217
|
+
const maxAttempts = this.options.creditWinnerMaxAttempts ?? 5;
|
|
218
|
+
if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > 100)
|
|
219
|
+
throw new domain_1.DomainError('Credit winner attempts must be an integer between 1 and 100', 503);
|
|
220
|
+
const tiers = (0, eligibility_1.giveawayEligibleCollectorTypes)(giveaway_2.GiveawayCategory.Credits);
|
|
221
|
+
const rejected = [];
|
|
222
|
+
const deadline = Date.now() + 30000;
|
|
223
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
224
|
+
if (Date.now() >= deadline)
|
|
225
|
+
throw new domain_1.DomainError('Membership verification is taking too long. Try the draw again.', 503);
|
|
226
|
+
const eligible = {
|
|
227
|
+
giveawayId: g._id,
|
|
228
|
+
_collectorType: { $in: [...tiers] },
|
|
229
|
+
...(rejected.length > 0 && { _id: { $nin: rejected } })
|
|
230
|
+
};
|
|
231
|
+
const count = await this.entries.countDocuments(eligible, {});
|
|
232
|
+
if (!count)
|
|
233
|
+
throw new domain_1.DomainError('This giveaway has no entries with an active eligible entitlement', 409);
|
|
234
|
+
const entry = input.method === giveaway_2.GiveawaySelectionMethod.Manual
|
|
235
|
+
? await this.entries.findOne({ ...eligible, _id: input.entryId }, {})
|
|
236
|
+
: await this.entries
|
|
237
|
+
.find(eligible, {
|
|
238
|
+
projection: { _id: 1, userId: 1, giveawayId: 1, _collectorType: 1 }
|
|
239
|
+
})
|
|
240
|
+
.sort({ _id: 1 })
|
|
241
|
+
.skip((0, node_crypto_1.randomInt)(count))
|
|
242
|
+
.limit(1)
|
|
243
|
+
.next();
|
|
244
|
+
if (!entry)
|
|
245
|
+
throw new domain_1.DomainError('Select a valid entry in this giveaway', 422);
|
|
246
|
+
if (!(0, eligibility_1.isGiveawayMemberEligible)(g.category, entry._collectorType))
|
|
247
|
+
throw new domain_1.DomainError('This entry is not eligible for the giveaway prize', 403);
|
|
248
|
+
const evidence = await this.checkCreditCandidate(entry);
|
|
249
|
+
if (Date.now() >= deadline)
|
|
250
|
+
throw new domain_1.DomainError('Membership verification is taking too long. Try the draw again.', 503);
|
|
251
|
+
if (evidence)
|
|
252
|
+
return { entryId: entry._id, evidence, skipped: rejected.length };
|
|
253
|
+
if (input.method === giveaway_2.GiveawaySelectionMethod.Manual)
|
|
254
|
+
throw new domain_1.DomainError('This entrant has no active Rookie, Collector or Elite entitlement', 422);
|
|
255
|
+
rejected.push(entry._id);
|
|
256
|
+
if (count === 1)
|
|
257
|
+
throw new domain_1.DomainError('This giveaway has no entries with an active eligible entitlement', 409);
|
|
258
|
+
}
|
|
259
|
+
throw new domain_1.DomainError(`No verified winner found after checking ${maxAttempts} candidate${maxAttempts === 1 ? '' : 's'}. Try the draw again.`, 503);
|
|
260
|
+
}
|
|
154
261
|
async transaction(operation) {
|
|
155
262
|
const session = this.client.startSession();
|
|
156
263
|
try {
|
|
@@ -579,6 +686,49 @@ class GiveawayRepository {
|
|
|
579
686
|
{ $sort: { _id: -1 } },
|
|
580
687
|
{ $skip: skip },
|
|
581
688
|
{ $limit: limit },
|
|
689
|
+
{
|
|
690
|
+
$lookup: {
|
|
691
|
+
from: 'notification_deliveries',
|
|
692
|
+
let: { notification: { $toString: '$_id' } },
|
|
693
|
+
pipeline: [
|
|
694
|
+
{ $match: { $expr: { $eq: ['$notificationId', '$$notification'] } } },
|
|
695
|
+
{
|
|
696
|
+
$group: {
|
|
697
|
+
_id: null,
|
|
698
|
+
queued: {
|
|
699
|
+
$sum: {
|
|
700
|
+
$cond: [
|
|
701
|
+
{ $eq: ['$status', giveaway_2.GiveawayNotificationDeliveryStatus.Queued] },
|
|
702
|
+
1,
|
|
703
|
+
0
|
|
704
|
+
]
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
skipped: {
|
|
708
|
+
$sum: {
|
|
709
|
+
$cond: [
|
|
710
|
+
{ $eq: ['$status', giveaway_2.GiveawayNotificationDeliveryStatus.Skipped] },
|
|
711
|
+
1,
|
|
712
|
+
0
|
|
713
|
+
]
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
pending: {
|
|
717
|
+
$sum: {
|
|
718
|
+
$cond: [
|
|
719
|
+
{ $eq: ['$status', giveaway_2.GiveawayNotificationDeliveryStatus.Pending] },
|
|
720
|
+
1,
|
|
721
|
+
0
|
|
722
|
+
]
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
{ $project: { _id: 0, queued: 1, skipped: 1, pending: 1 } }
|
|
728
|
+
],
|
|
729
|
+
as: 'deliveryTotals'
|
|
730
|
+
}
|
|
731
|
+
},
|
|
582
732
|
{
|
|
583
733
|
$lookup: {
|
|
584
734
|
from: 'giveaways',
|
|
@@ -594,10 +744,13 @@ class GiveawayRepository {
|
|
|
594
744
|
$set: {
|
|
595
745
|
giveawayTitle: {
|
|
596
746
|
$ifNull: [{ $first: '$campaign.title' }, '$giveawayId']
|
|
747
|
+
},
|
|
748
|
+
deliveryCounts: {
|
|
749
|
+
$ifNull: [{ $first: '$deliveryTotals' }, { queued: 0, skipped: 0, pending: 0 }]
|
|
597
750
|
}
|
|
598
751
|
}
|
|
599
752
|
},
|
|
600
|
-
{ $unset: ['campaign', 'claimToken'] },
|
|
753
|
+
{ $unset: ['campaign', 'claimToken', 'deliveryTotals'] },
|
|
601
754
|
stringId
|
|
602
755
|
])
|
|
603
756
|
.toArray(),
|
|
@@ -741,6 +894,20 @@ class GiveawayRepository {
|
|
|
741
894
|
}
|
|
742
895
|
async confirmWinners(actor, id, raw) {
|
|
743
896
|
const input = schemas_1.assignmentsSchema.parse(raw);
|
|
897
|
+
const reviewed = await this.getGiveaway(id);
|
|
898
|
+
let verified;
|
|
899
|
+
if (reviewed.category === giveaway_2.GiveawayCategory.Credits) {
|
|
900
|
+
const rows = await this.entries
|
|
901
|
+
.find({ giveawayId: id, _id: { $in: input.assignments.map(a => a.entryId) } }, { projection: { _id: 1, userId: 1, giveawayId: 1, _collectorType: 1 } })
|
|
902
|
+
.limit(1)
|
|
903
|
+
.toArray();
|
|
904
|
+
const existing = await this.winners.findOne({ giveawayId: id, replacedAt: null }, { projection: { _id: 1 } });
|
|
905
|
+
(0, domain_1.confirmWinners)(reviewed, rows, !!existing, actor, input);
|
|
906
|
+
const evidence = await this.checkCreditCandidate(rows[0]);
|
|
907
|
+
if (!evidence)
|
|
908
|
+
throw new domain_1.DomainError('This entrant has no active Rookie, Collector or Elite entitlement', 422);
|
|
909
|
+
verified = { entryId: rows[0]._id, evidence, skipped: 0 };
|
|
910
|
+
}
|
|
744
911
|
return this.transaction(async (session) => {
|
|
745
912
|
const g = await this.getGiveaway(id, session);
|
|
746
913
|
const rows = await this.entries
|
|
@@ -749,17 +916,25 @@ class GiveawayRepository {
|
|
|
749
916
|
_id: { $in: input.assignments.map(a => a.entryId) }
|
|
750
917
|
}, {
|
|
751
918
|
session,
|
|
752
|
-
projection: { _id: 1, userId: 1, giveawayId: 1 }
|
|
919
|
+
projection: { _id: 1, userId: 1, giveawayId: 1, _collectorType: 1 }
|
|
753
920
|
})
|
|
754
921
|
.limit(1000)
|
|
755
922
|
.toArray();
|
|
756
923
|
const entries = rows;
|
|
757
924
|
const existing = await this.winners.findOne({ giveawayId: id, replacedAt: null }, { session, projection: { _id: 1 } });
|
|
758
925
|
const winners = (0, domain_1.confirmWinners)(g, entries, !!existing, actor, input);
|
|
926
|
+
this.assertVerifiedCreditCandidate(g, reviewed, entries[0], verified);
|
|
927
|
+
if (verified)
|
|
928
|
+
winners[0].creditEligibility = verified.evidence;
|
|
759
929
|
await this.updateGiveaway({ ...g, status: giveaway_2.GiveawayStatus.AwaitingClaim, revision: g.revision + 1 }, g.revision, session);
|
|
760
930
|
const result = await this.winners.insertMany(winners.map(winner => ({ ...winner })), { session });
|
|
761
|
-
await this.enqueueEvent(g, giveaway_2.GiveawayNotificationKind.Results, 'results:' + result.insertedIds[0].toHexString(), session);
|
|
762
|
-
await this.audit(actor, 'Winners confirmed', id,
|
|
931
|
+
await this.enqueueEvent(g, giveaway_2.GiveawayNotificationKind.Results, 'results:' + result.insertedIds[0].toHexString(), session, winners[0].claimDeadline);
|
|
932
|
+
await this.audit(actor, 'Winners confirmed', id, verified
|
|
933
|
+
? JSON.stringify({
|
|
934
|
+
prizeAssignments: winners.length,
|
|
935
|
+
creditEligibility: verified.evidence
|
|
936
|
+
})
|
|
937
|
+
: winners.length + ' prize assignments', session);
|
|
763
938
|
return winners.map((winner, index) => ({
|
|
764
939
|
...winner,
|
|
765
940
|
_id: result.insertedIds[index].toHexString()
|
|
@@ -767,22 +942,52 @@ class GiveawayRepository {
|
|
|
767
942
|
});
|
|
768
943
|
}
|
|
769
944
|
async replaceWinner(actor, id, winnerId, replacementEntryId, reason) {
|
|
945
|
+
const reviewed = await this.getGiveaway(id);
|
|
946
|
+
let verified;
|
|
947
|
+
if (reviewed.category === giveaway_2.GiveawayCategory.Credits) {
|
|
948
|
+
const previous = await this.winners.findOne({
|
|
949
|
+
_id: objectId(winnerId),
|
|
950
|
+
giveawayId: id,
|
|
951
|
+
replacedAt: null
|
|
952
|
+
});
|
|
953
|
+
const entry = await this.entries.findOne({ _id: schemas_1.entryIdSchema.parse(replacementEntryId), giveawayId: id }, { projection: { _id: 1, userId: 1, giveawayId: 1, _collectorType: 1 } });
|
|
954
|
+
if (!previous || !entry)
|
|
955
|
+
throw new domain_1.DomainError('Winner or entrant not found', 404);
|
|
956
|
+
const existing = await this.winners.findOne({ giveawayId: id, entryId: replacementEntryId, replacedAt: null }, { projection: { _id: 1 } });
|
|
957
|
+
(0, domain_1.replaceWinner)(reviewed, serialize(previous), entry, !!existing, actor, reason);
|
|
958
|
+
const evidence = await this.checkCreditCandidate(entry);
|
|
959
|
+
if (!evidence)
|
|
960
|
+
throw new domain_1.DomainError('This entrant has no active Rookie, Collector or Elite entitlement', 422);
|
|
961
|
+
verified = { entryId: entry._id, evidence, skipped: 0 };
|
|
962
|
+
}
|
|
770
963
|
return this.transaction(async (session) => {
|
|
771
964
|
const g = await this.getGiveaway(id, session);
|
|
772
965
|
const previousWinner = await this.winners.findOne({ _id: objectId(winnerId), giveawayId: id, replacedAt: null }, { session });
|
|
773
|
-
const entrant = await this.entries.findOne({ _id: schemas_1.entryIdSchema.parse(replacementEntryId), giveawayId: id }, { session, projection: { _id: 1, giveawayId: 1 } });
|
|
966
|
+
const entrant = await this.entries.findOne({ _id: schemas_1.entryIdSchema.parse(replacementEntryId), giveawayId: id }, { session, projection: { _id: 1, userId: 1, giveawayId: 1, _collectorType: 1 } });
|
|
774
967
|
if (!previousWinner || !entrant)
|
|
775
968
|
throw new domain_1.DomainError('Winner or entrant not found', 404);
|
|
776
969
|
const old = serialize(previousWinner);
|
|
777
970
|
const entry = entrant;
|
|
778
971
|
const existing = await this.winners.findOne({ giveawayId: id, entryId: replacementEntryId, replacedAt: null }, { session, projection: { _id: 1 } });
|
|
779
972
|
const { previous, replacement } = (0, domain_1.replaceWinner)(g, old, entry, !!existing, actor, reason);
|
|
973
|
+
this.assertVerifiedCreditCandidate(g, reviewed, entry, verified);
|
|
974
|
+
replacement.userId = entry.userId;
|
|
975
|
+
if (verified) {
|
|
976
|
+
replacement.creditEligibility = verified.evidence;
|
|
977
|
+
replacement.selection.creditEligibility = verified.evidence;
|
|
978
|
+
}
|
|
780
979
|
await this.updateGiveaway({ ...g, status: giveaway_2.GiveawayStatus.AwaitingClaim, revision: g.revision + 1 }, g.revision, session);
|
|
781
980
|
const { _id, ...previousFields } = previous;
|
|
782
981
|
await this.winners.updateOne({ _id: objectId(_id), replacedAt: null }, { $set: previousFields }, { session });
|
|
783
982
|
const result = await this.winners.insertOne({ ...replacement }, { session });
|
|
784
|
-
await this.enqueueEvent(g, giveaway_2.GiveawayNotificationKind.Results, 'results:' + result.insertedId.toHexString(), session);
|
|
785
|
-
await this.audit(actor, 'Winner replaced', id,
|
|
983
|
+
await this.enqueueEvent(g, giveaway_2.GiveawayNotificationKind.Results, 'results:' + result.insertedId.toHexString(), session, replacement.claimDeadline);
|
|
984
|
+
await this.audit(actor, 'Winner replaced', id, verified
|
|
985
|
+
? JSON.stringify({
|
|
986
|
+
previousWinnerId: old._id,
|
|
987
|
+
reason: reason.trim(),
|
|
988
|
+
creditEligibility: verified.evidence
|
|
989
|
+
})
|
|
990
|
+
: old._id + ': ' + reason.trim(), session);
|
|
786
991
|
return { ...replacement, _id: result.insertedId.toHexString() };
|
|
787
992
|
});
|
|
788
993
|
}
|
|
@@ -818,7 +1023,7 @@ class GiveawayRepository {
|
|
|
818
1023
|
return winner;
|
|
819
1024
|
});
|
|
820
1025
|
}
|
|
821
|
-
async enqueueEvent(g, kind, eventKey, session) {
|
|
1026
|
+
async enqueueEvent(g, kind, eventKey, session, expiresAt) {
|
|
822
1027
|
const copy = g.reminders?.[kind];
|
|
823
1028
|
if (!copy?.enabled)
|
|
824
1029
|
return;
|
|
@@ -828,6 +1033,11 @@ class GiveawayRepository {
|
|
|
828
1033
|
eventKey,
|
|
829
1034
|
kind,
|
|
830
1035
|
audience: giveaway_2.GiveawayNotificationAudience.Entrants,
|
|
1036
|
+
category: g.category,
|
|
1037
|
+
expiresAt: kind === giveaway_2.GiveawayNotificationKind.Closing
|
|
1038
|
+
? g.closesAt
|
|
1039
|
+
: expiresAt ||
|
|
1040
|
+
new Date(Date.now() + (g.claimWindowDays || 14) * 86400000).toISOString(),
|
|
831
1041
|
title: copy.title,
|
|
832
1042
|
message: copy.message,
|
|
833
1043
|
status: giveaway_2.GiveawayNotificationStatus.Pending,
|
|
@@ -837,6 +1047,10 @@ class GiveawayRepository {
|
|
|
837
1047
|
}
|
|
838
1048
|
async selectWinner(actor, id, raw) {
|
|
839
1049
|
const input = schemas_1.selectionSchema.parse(raw);
|
|
1050
|
+
const reviewed = await this.getGiveaway(id);
|
|
1051
|
+
const verified = reviewed.category === giveaway_2.GiveawayCategory.Credits
|
|
1052
|
+
? await this.verifiedCreditSelection(reviewed, input)
|
|
1053
|
+
: undefined;
|
|
840
1054
|
return this.transaction(async (session) => {
|
|
841
1055
|
const g = await this.getGiveaway(id, session);
|
|
842
1056
|
if ((0, domain_1.effectiveStatus)(g) !== giveaway_2.GiveawayStatus.Closed)
|
|
@@ -846,29 +1060,47 @@ class GiveawayRepository {
|
|
|
846
1060
|
if (g.selection ||
|
|
847
1061
|
(await this.winners.findOne({ giveawayId: id, replacedAt: null }, { session })))
|
|
848
1062
|
throw new domain_1.DomainError('A winner is already selected. Explicitly reset the selection to change it.', 409);
|
|
849
|
-
const
|
|
1063
|
+
const collectorTypes = (0, eligibility_1.giveawayEligibleCollectorTypes)(g.category);
|
|
1064
|
+
const eligible = {
|
|
1065
|
+
giveawayId: id,
|
|
1066
|
+
...(collectorTypes !== null && { _collectorType: { $in: [...collectorTypes] } })
|
|
1067
|
+
};
|
|
1068
|
+
const count = verified ? 1 : await this.entries.countDocuments(eligible, { session });
|
|
850
1069
|
if (!count)
|
|
851
1070
|
throw new domain_1.DomainError('This giveaway has no eligible entries', 409);
|
|
852
|
-
const
|
|
853
|
-
|
|
1071
|
+
const candidateId = verified?.entryId ||
|
|
1072
|
+
(input.method === giveaway_2.GiveawaySelectionMethod.Manual ? input.entryId : undefined);
|
|
1073
|
+
const entry = candidateId
|
|
1074
|
+
? await this.entries.findOne({ ...eligible, _id: candidateId }, { session })
|
|
854
1075
|
: await this.entries
|
|
855
|
-
.find(
|
|
1076
|
+
.find(eligible, { session })
|
|
856
1077
|
.sort({ _id: 1 })
|
|
857
1078
|
.skip((0, node_crypto_1.randomInt)(count))
|
|
858
1079
|
.limit(1)
|
|
859
1080
|
.next();
|
|
860
1081
|
if (!entry)
|
|
861
1082
|
throw new domain_1.DomainError('Select a valid entry in this giveaway', 422);
|
|
1083
|
+
if (!(0, eligibility_1.isGiveawayMemberEligible)(g.category, entry._collectorType))
|
|
1084
|
+
throw new domain_1.DomainError('This entry is not eligible for the giveaway prize', 403);
|
|
1085
|
+
this.assertVerifiedCreditCandidate(g, reviewed, entry, verified);
|
|
862
1086
|
const selection = {
|
|
863
1087
|
entryId: entry._id,
|
|
864
1088
|
method: input.method,
|
|
865
1089
|
reason: input.method === giveaway_2.GiveawaySelectionMethod.Manual ? input.reason : '',
|
|
866
1090
|
selectedBy: actor,
|
|
867
|
-
selectedAt: new Date().toISOString()
|
|
1091
|
+
selectedAt: new Date().toISOString(),
|
|
1092
|
+
...(verified && { creditEligibility: verified.evidence })
|
|
868
1093
|
};
|
|
869
1094
|
const updated = { ...g, selection, revision: g.revision + 1 };
|
|
870
1095
|
await this.updateGiveaway(updated, g.revision, session);
|
|
871
|
-
await this.audit(actor, 'Winner selected: ' + input.method, id,
|
|
1096
|
+
await this.audit(actor, 'Winner selected: ' + input.method, id, verified
|
|
1097
|
+
? JSON.stringify({
|
|
1098
|
+
entryId: entry._id,
|
|
1099
|
+
reason: selection.reason,
|
|
1100
|
+
creditEligibility: verified.evidence,
|
|
1101
|
+
skippedIneligibleEntries: verified.skipped
|
|
1102
|
+
})
|
|
1103
|
+
: entry._id + (selection.reason ? ': ' + selection.reason : ''), session);
|
|
872
1104
|
return { giveaway: updated, entry: normalizeEntry(entry) };
|
|
873
1105
|
});
|
|
874
1106
|
}
|
|
@@ -930,7 +1162,7 @@ class GiveawayRepository {
|
|
|
930
1162
|
category: giveaway_2.GiveawayCategory.Credits,
|
|
931
1163
|
creditAmount: amount,
|
|
932
1164
|
countries: [],
|
|
933
|
-
rules: 'One ticket per DGC account.
|
|
1165
|
+
rules: 'One ticket per eligible DGC account. Eligible members have equal chances. One winner. The winner must claim within the stated claim window.',
|
|
934
1166
|
deliveryInformation: '',
|
|
935
1167
|
revision: 1,
|
|
936
1168
|
createdAt: now,
|
|
@@ -1263,7 +1495,11 @@ class GiveawayRepository {
|
|
|
1263
1495
|
const now = new Date().toISOString();
|
|
1264
1496
|
const updated = await this.notifications.updateOne(filter, successful
|
|
1265
1497
|
? {
|
|
1266
|
-
$set: {
|
|
1498
|
+
$set: {
|
|
1499
|
+
status: giveaway_2.GiveawayNotificationStatus.Sent,
|
|
1500
|
+
sentAt: now,
|
|
1501
|
+
dispatchCompletedAt: now
|
|
1502
|
+
},
|
|
1267
1503
|
$unset: { error: '', claimToken: '' }
|
|
1268
1504
|
}
|
|
1269
1505
|
: {
|
package/dist/giveaways/index.js
CHANGED
|
@@ -18,6 +18,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
18
18
|
__exportStar(require("./domain"), exports);
|
|
19
19
|
__exportStar(require("./api-pagination"), exports);
|
|
20
20
|
__exportStar(require("./entry-id"), exports);
|
|
21
|
+
__exportStar(require("./eligibility"), exports);
|
|
21
22
|
__exportStar(require("./giveaway-repository"), exports);
|
|
22
23
|
__exportStar(require("./schemas"), exports);
|
|
23
24
|
__exportStar(require("./credit-award"), exports);
|
package/dist/index.d.ts
CHANGED
|
@@ -2,8 +2,6 @@ export * from './types';
|
|
|
2
2
|
export * from './db/mongo';
|
|
3
3
|
export * from './db/mongo2';
|
|
4
4
|
export * from './db/mongo3';
|
|
5
|
-
export * from './tw/utils';
|
|
6
|
-
export * from './tw/wallet';
|
|
7
5
|
export * from './auth';
|
|
8
6
|
export * from './chunk';
|
|
9
7
|
export * from './env';
|
|
@@ -20,3 +18,5 @@ export * from './validator';
|
|
|
20
18
|
export * from './id';
|
|
21
19
|
export * from './http-error';
|
|
22
20
|
export * from './giveaways';
|
|
21
|
+
export * from './constants/revenuecat';
|
|
22
|
+
export * from './revenuecat';
|
package/dist/index.js
CHANGED
|
@@ -18,8 +18,6 @@ __exportStar(require("./types"), exports);
|
|
|
18
18
|
__exportStar(require("./db/mongo"), exports);
|
|
19
19
|
__exportStar(require("./db/mongo2"), exports);
|
|
20
20
|
__exportStar(require("./db/mongo3"), exports);
|
|
21
|
-
__exportStar(require("./tw/utils"), exports);
|
|
22
|
-
__exportStar(require("./tw/wallet"), exports);
|
|
23
21
|
__exportStar(require("./auth"), exports);
|
|
24
22
|
__exportStar(require("./chunk"), exports);
|
|
25
23
|
__exportStar(require("./env"), exports);
|
|
@@ -36,3 +34,5 @@ __exportStar(require("./validator"), exports);
|
|
|
36
34
|
__exportStar(require("./id"), exports);
|
|
37
35
|
__exportStar(require("./http-error"), exports);
|
|
38
36
|
__exportStar(require("./giveaways"), exports);
|
|
37
|
+
__exportStar(require("./constants/revenuecat"), exports);
|
|
38
|
+
__exportStar(require("./revenuecat"), exports);
|
package/dist/pub.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.publishMessage = publishMessage;
|
|
4
4
|
// src/pub.ts
|
|
5
5
|
const pubsub_1 = require("@google-cloud/pubsub");
|
|
6
|
+
const logger_1 = require("./logger");
|
|
6
7
|
let cachedPubSub = null;
|
|
7
8
|
function getPubSub(projectId) {
|
|
8
9
|
if (!projectId)
|
|
@@ -26,11 +27,11 @@ async function publishMessage(projectId, topicName, envelope) {
|
|
|
26
27
|
const messageId = await topic.publishMessage({
|
|
27
28
|
data
|
|
28
29
|
});
|
|
29
|
-
|
|
30
|
+
(0, logger_1.log)(logger_1.LogLevel.INFO, 'Pub/Sub publish', { topicName, messageId });
|
|
30
31
|
return messageId;
|
|
31
32
|
}
|
|
32
33
|
catch (err) {
|
|
33
|
-
|
|
34
|
+
(0, logger_1.log)(logger_1.LogLevel.ERROR, 'publish failed', { error: err });
|
|
34
35
|
throw err;
|
|
35
36
|
}
|
|
36
37
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { HttpError } from './http-error';
|
|
2
|
+
import type { GiveawayCreditEligibilityEvidence, RevenueCatClient, RevenueCatEntitlementIds } from './types/revenuecat';
|
|
3
|
+
export type RevenueCatClientOptions = {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
projectId: string;
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
totalTimeoutMs?: number;
|
|
8
|
+
maxPages?: number;
|
|
9
|
+
};
|
|
10
|
+
/** Deliberately excludes the original HTTP error, which can contain the API key. */
|
|
11
|
+
export declare class RevenueCatLookupError extends HttpError {
|
|
12
|
+
constructor();
|
|
13
|
+
}
|
|
14
|
+
export declare function createRevenueCatClient(options: RevenueCatClientOptions): RevenueCatClient;
|
|
15
|
+
/** Resolve any eligible entitlement from one customer lookup; never infer Free from API failures. */
|
|
16
|
+
export declare function verifyCreditGiveawayEntitlement(userId: string, client: RevenueCatClient, entitlementIds: RevenueCatEntitlementIds): Promise<GiveawayCreditEligibilityEvidence | null>;
|