@withwiz/coupon 0.1.0
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/LICENSE +21 -0
- package/README.md +298 -0
- package/dist/index.cjs +1928 -0
- package/dist/index.d.cts +755 -0
- package/dist/index.d.ts +755 -0
- package/dist/index.mjs +1897 -0
- package/package.json +86 -0
- package/prisma/fragment.prisma +122 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain-level constants for @withwiz/coupon.
|
|
3
|
+
* No env vars, no side effects. Pure values.
|
|
4
|
+
*/
|
|
5
|
+
declare const COUPON_STATUS: {
|
|
6
|
+
readonly DRAFT: "DRAFT";
|
|
7
|
+
readonly ACTIVE: "ACTIVE";
|
|
8
|
+
readonly PAUSED: "PAUSED";
|
|
9
|
+
readonly EXPIRED: "EXPIRED";
|
|
10
|
+
readonly EXHAUSTED: "EXHAUSTED";
|
|
11
|
+
readonly ARCHIVED: "ARCHIVED";
|
|
12
|
+
};
|
|
13
|
+
type CouponStatus = (typeof COUPON_STATUS)[keyof typeof COUPON_STATUS];
|
|
14
|
+
declare const CAMPAIGN_STATUS: {
|
|
15
|
+
readonly ACTIVE: "ACTIVE";
|
|
16
|
+
readonly CLOSED: "CLOSED";
|
|
17
|
+
};
|
|
18
|
+
type CampaignStatus = (typeof CAMPAIGN_STATUS)[keyof typeof CAMPAIGN_STATUS];
|
|
19
|
+
/**
|
|
20
|
+
* Closed union of validate() reject reasons.
|
|
21
|
+
* Exactly 13 literals as frozen by sprint_contract.md §7.4.
|
|
22
|
+
*/
|
|
23
|
+
declare const COUPON_REJECT_REASON: {
|
|
24
|
+
readonly NOT_FOUND: "NOT_FOUND";
|
|
25
|
+
readonly NOT_STARTED: "NOT_STARTED";
|
|
26
|
+
readonly EXPIRED: "EXPIRED";
|
|
27
|
+
readonly EXHAUSTED: "EXHAUSTED";
|
|
28
|
+
readonly PER_USER_LIMIT: "PER_USER_LIMIT";
|
|
29
|
+
readonly MIN_ORDER_NOT_MET: "MIN_ORDER_NOT_MET";
|
|
30
|
+
readonly INELIGIBLE_USER: "INELIGIBLE_USER";
|
|
31
|
+
readonly INELIGIBLE_PLAN: "INELIGIBLE_PLAN";
|
|
32
|
+
readonly INELIGIBLE_PRODUCT: "INELIGIBLE_PRODUCT";
|
|
33
|
+
readonly INELIGIBLE_CATEGORY: "INELIGIBLE_CATEGORY";
|
|
34
|
+
readonly PAUSED: "PAUSED";
|
|
35
|
+
readonly ARCHIVED: "ARCHIVED";
|
|
36
|
+
readonly TENANT_MISMATCH: "TENANT_MISMATCH";
|
|
37
|
+
};
|
|
38
|
+
type CouponRejectReason = (typeof COUPON_REJECT_REASON)[keyof typeof COUPON_REJECT_REASON];
|
|
39
|
+
/**
|
|
40
|
+
* Sanitization and resource limits.
|
|
41
|
+
* - [A] code length cap / allowed charset
|
|
42
|
+
* - [C] tenantId format
|
|
43
|
+
* - [I] bulk count cap
|
|
44
|
+
* - [J] maxAttempts cap
|
|
45
|
+
* - [K] cursor pagination limits
|
|
46
|
+
*/
|
|
47
|
+
declare const LIMITS: {
|
|
48
|
+
readonly CODE_MAX_LENGTH: 64;
|
|
49
|
+
readonly CODE_MIN_LENGTH: 1;
|
|
50
|
+
/**
|
|
51
|
+
* Default character set for generated codes (confusing chars removed).
|
|
52
|
+
* Excludes `0/O`, `1/I/L` and lower-case to avoid user transcription errors.
|
|
53
|
+
*/
|
|
54
|
+
readonly CODE_DEFAULT_ALPHABET: "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
|
|
55
|
+
readonly CODE_DEFAULT_LENGTH: 8;
|
|
56
|
+
/**
|
|
57
|
+
* [A] whitelist for accepted coupon codes (after NFKC normalization + upper-case).
|
|
58
|
+
* Only uppercase ASCII letters, digits, underscore, hyphen are allowed.
|
|
59
|
+
* SQL meta characters / quotes / spaces / CJK / emoji are all rejected.
|
|
60
|
+
*/
|
|
61
|
+
readonly CODE_ALLOWED_REGEX: RegExp;
|
|
62
|
+
readonly TENANT_ID_MAX_LENGTH: 64;
|
|
63
|
+
readonly TENANT_ID_REGEX: RegExp;
|
|
64
|
+
readonly BULK_MAX_COUNT: 10000;
|
|
65
|
+
readonly BULK_MAX_ATTEMPTS: 5;
|
|
66
|
+
readonly BULK_DEFAULT_MAX_ATTEMPTS: 3;
|
|
67
|
+
readonly PAGINATION_DEFAULT_LIMIT: 50;
|
|
68
|
+
readonly PAGINATION_MAX_LIMIT: 200;
|
|
69
|
+
readonly NAME_MAX_LENGTH: 200;
|
|
70
|
+
readonly CURRENCY_LENGTH: 3;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Audit log action strings (stable identifiers).
|
|
74
|
+
*/
|
|
75
|
+
declare const AUDIT_ACTION: {
|
|
76
|
+
readonly COUPON_CREATED: "coupon.created";
|
|
77
|
+
readonly COUPON_PAUSED: "coupon.paused";
|
|
78
|
+
readonly COUPON_RESUMED: "coupon.resumed";
|
|
79
|
+
readonly COUPON_ARCHIVED: "coupon.archived";
|
|
80
|
+
readonly COUPON_ISSUED: "coupon.issued";
|
|
81
|
+
readonly COUPON_ISSUED_BULK: "coupon.issued.bulk";
|
|
82
|
+
readonly COUPON_REDEEMED: "coupon.redeemed";
|
|
83
|
+
readonly COUPON_REDEEM_REJECTED: "coupon.redeem.rejected";
|
|
84
|
+
readonly CAMPAIGN_CREATED: "campaign.created";
|
|
85
|
+
readonly CAMPAIGN_CLOSED: "campaign.closed";
|
|
86
|
+
};
|
|
87
|
+
type AuditAction = (typeof AUDIT_ACTION)[keyof typeof AUDIT_ACTION];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Public DTO & domain types for @withwiz/coupon.
|
|
91
|
+
* No Prisma generated types imported here (see contract §10 PrismaLike rule).
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
type DiscountPolicy = {
|
|
95
|
+
kind: "PERCENT";
|
|
96
|
+
percent: number;
|
|
97
|
+
maxDiscount?: number;
|
|
98
|
+
minOrder?: number;
|
|
99
|
+
currency?: string;
|
|
100
|
+
} | {
|
|
101
|
+
kind: "FIXED";
|
|
102
|
+
amount: number;
|
|
103
|
+
currency: string;
|
|
104
|
+
minOrder?: number;
|
|
105
|
+
};
|
|
106
|
+
type EligibilityRules = {
|
|
107
|
+
allowedUserIds?: string[];
|
|
108
|
+
allowedPlans?: string[];
|
|
109
|
+
allowedProductIds?: string[];
|
|
110
|
+
allowedCategories?: string[];
|
|
111
|
+
};
|
|
112
|
+
type Coupon = {
|
|
113
|
+
id: string;
|
|
114
|
+
tenantId: string;
|
|
115
|
+
campaignId: string | null;
|
|
116
|
+
code: string;
|
|
117
|
+
status: CouponStatus;
|
|
118
|
+
discount: DiscountPolicy;
|
|
119
|
+
startsAt: Date | null;
|
|
120
|
+
endsAt: Date | null;
|
|
121
|
+
maxRedemptions: number | null;
|
|
122
|
+
redeemedCount: number;
|
|
123
|
+
maxRedemptionsPerUser: number | null;
|
|
124
|
+
eligibility: EligibilityRules;
|
|
125
|
+
metadata: Record<string, unknown> | null;
|
|
126
|
+
createdAt: Date;
|
|
127
|
+
updatedAt: Date;
|
|
128
|
+
};
|
|
129
|
+
type Campaign = {
|
|
130
|
+
id: string;
|
|
131
|
+
tenantId: string;
|
|
132
|
+
name: string;
|
|
133
|
+
status: CampaignStatus;
|
|
134
|
+
discount: DiscountPolicy;
|
|
135
|
+
eligibility: EligibilityRules;
|
|
136
|
+
startsAt: Date | null;
|
|
137
|
+
endsAt: Date | null;
|
|
138
|
+
maxCoupons: number | null;
|
|
139
|
+
issuedCount: number;
|
|
140
|
+
metadata: Record<string, unknown> | null;
|
|
141
|
+
createdAt: Date;
|
|
142
|
+
updatedAt: Date;
|
|
143
|
+
};
|
|
144
|
+
type Issuance = {
|
|
145
|
+
id: string;
|
|
146
|
+
tenantId: string;
|
|
147
|
+
couponId: string;
|
|
148
|
+
issuedToUserId: string | null;
|
|
149
|
+
issuedBy: string | null;
|
|
150
|
+
issuedAt: Date;
|
|
151
|
+
metadata: Record<string, unknown> | null;
|
|
152
|
+
};
|
|
153
|
+
type Redemption = {
|
|
154
|
+
id: string;
|
|
155
|
+
tenantId: string;
|
|
156
|
+
couponId: string;
|
|
157
|
+
redeemedByUserId: string | null;
|
|
158
|
+
orderRef: string | null;
|
|
159
|
+
discountAmount: number;
|
|
160
|
+
currency: string;
|
|
161
|
+
redeemedAt: Date;
|
|
162
|
+
metadata: Record<string, unknown> | null;
|
|
163
|
+
};
|
|
164
|
+
type AuditLog = {
|
|
165
|
+
id: string;
|
|
166
|
+
tenantId: string;
|
|
167
|
+
action: string;
|
|
168
|
+
resourceId: string | null;
|
|
169
|
+
resourceType: string | null;
|
|
170
|
+
actorId: string | null;
|
|
171
|
+
details: Record<string, unknown> | null;
|
|
172
|
+
createdAt: Date;
|
|
173
|
+
};
|
|
174
|
+
type CreateCouponInput = {
|
|
175
|
+
tenantId?: string;
|
|
176
|
+
code?: string;
|
|
177
|
+
codeLength?: number;
|
|
178
|
+
campaignId?: string;
|
|
179
|
+
discount: DiscountPolicy;
|
|
180
|
+
startsAt?: Date | null;
|
|
181
|
+
endsAt?: Date | null;
|
|
182
|
+
maxRedemptions?: number | null;
|
|
183
|
+
maxRedemptionsPerUser?: number | null;
|
|
184
|
+
eligibility?: EligibilityRules;
|
|
185
|
+
metadata?: Record<string, unknown> | null;
|
|
186
|
+
status?: CouponStatus;
|
|
187
|
+
actorId?: string;
|
|
188
|
+
};
|
|
189
|
+
type IssueCouponInput = {
|
|
190
|
+
tenantId?: string;
|
|
191
|
+
couponId: string;
|
|
192
|
+
issuedToUserId?: string | null;
|
|
193
|
+
issuedBy?: string | null;
|
|
194
|
+
metadata?: Record<string, unknown> | null;
|
|
195
|
+
};
|
|
196
|
+
type IssueBulkInput = {
|
|
197
|
+
tenantId?: string;
|
|
198
|
+
count: number;
|
|
199
|
+
codeLength?: number;
|
|
200
|
+
campaignId?: string;
|
|
201
|
+
discount: DiscountPolicy;
|
|
202
|
+
startsAt?: Date | null;
|
|
203
|
+
endsAt?: Date | null;
|
|
204
|
+
maxRedemptions?: number | null;
|
|
205
|
+
maxRedemptionsPerUser?: number | null;
|
|
206
|
+
eligibility?: EligibilityRules;
|
|
207
|
+
metadata?: Record<string, unknown> | null;
|
|
208
|
+
maxAttempts?: number;
|
|
209
|
+
actorId?: string;
|
|
210
|
+
};
|
|
211
|
+
type BulkIssuanceResult = {
|
|
212
|
+
created: Coupon[];
|
|
213
|
+
skipped: number;
|
|
214
|
+
codes: string[];
|
|
215
|
+
attempts: number;
|
|
216
|
+
};
|
|
217
|
+
type ListCouponsArgs = {
|
|
218
|
+
tenantId?: string;
|
|
219
|
+
campaignId?: string;
|
|
220
|
+
status?: CouponStatus;
|
|
221
|
+
cursor?: string;
|
|
222
|
+
limit?: number;
|
|
223
|
+
};
|
|
224
|
+
type ListCampaignsArgs = {
|
|
225
|
+
tenantId?: string;
|
|
226
|
+
status?: CampaignStatus;
|
|
227
|
+
cursor?: string;
|
|
228
|
+
limit?: number;
|
|
229
|
+
};
|
|
230
|
+
type ListIssuancesArgs = {
|
|
231
|
+
tenantId?: string;
|
|
232
|
+
couponId?: string;
|
|
233
|
+
issuedToUserId?: string;
|
|
234
|
+
cursor?: string;
|
|
235
|
+
limit?: number;
|
|
236
|
+
};
|
|
237
|
+
type ListRedemptionsArgs = {
|
|
238
|
+
tenantId?: string;
|
|
239
|
+
couponId?: string;
|
|
240
|
+
redeemedByUserId?: string;
|
|
241
|
+
cursor?: string;
|
|
242
|
+
limit?: number;
|
|
243
|
+
};
|
|
244
|
+
type ListAuditLogsArgs = {
|
|
245
|
+
tenantId?: string;
|
|
246
|
+
action?: string;
|
|
247
|
+
resourceId?: string;
|
|
248
|
+
resourceType?: string;
|
|
249
|
+
cursor?: string;
|
|
250
|
+
limit?: number;
|
|
251
|
+
};
|
|
252
|
+
type Paged<T> = {
|
|
253
|
+
items: T[];
|
|
254
|
+
nextCursor: string | null;
|
|
255
|
+
};
|
|
256
|
+
type ValidateContext = {
|
|
257
|
+
plan?: string;
|
|
258
|
+
productId?: string;
|
|
259
|
+
categoryId?: string;
|
|
260
|
+
};
|
|
261
|
+
type ValidateInput = {
|
|
262
|
+
tenantId?: string;
|
|
263
|
+
code: string;
|
|
264
|
+
userId?: string | null;
|
|
265
|
+
subtotal?: number;
|
|
266
|
+
currency?: string;
|
|
267
|
+
context?: ValidateContext;
|
|
268
|
+
};
|
|
269
|
+
type ValidationResult = {
|
|
270
|
+
ok: true;
|
|
271
|
+
coupon: Coupon;
|
|
272
|
+
discountPreview: DiscountBreakdown | null;
|
|
273
|
+
} | {
|
|
274
|
+
ok: false;
|
|
275
|
+
reason: CouponRejectReason;
|
|
276
|
+
};
|
|
277
|
+
type DiscountInput = {
|
|
278
|
+
subtotal: number;
|
|
279
|
+
currency: string;
|
|
280
|
+
policy: DiscountPolicy;
|
|
281
|
+
};
|
|
282
|
+
type DiscountBreakdown = {
|
|
283
|
+
subtotal: number;
|
|
284
|
+
discountAmount: number;
|
|
285
|
+
total: number;
|
|
286
|
+
appliedPolicy: DiscountPolicy;
|
|
287
|
+
trace: string[];
|
|
288
|
+
currency: string;
|
|
289
|
+
};
|
|
290
|
+
type RedeemInput = {
|
|
291
|
+
tenantId?: string;
|
|
292
|
+
code: string;
|
|
293
|
+
userId?: string | null;
|
|
294
|
+
orderRef?: string | null;
|
|
295
|
+
subtotal: number;
|
|
296
|
+
currency: string;
|
|
297
|
+
context?: ValidateContext;
|
|
298
|
+
metadata?: Record<string, unknown> | null;
|
|
299
|
+
/**
|
|
300
|
+
* Consumer's Prisma transaction client. When provided, redeem() "piggybacks"
|
|
301
|
+
* on this transaction instead of opening a new one.
|
|
302
|
+
*/
|
|
303
|
+
tx?: PrismaTxLike;
|
|
304
|
+
};
|
|
305
|
+
type RedemptionResult = {
|
|
306
|
+
redemption: Redemption;
|
|
307
|
+
coupon: Coupon;
|
|
308
|
+
discount: DiscountBreakdown;
|
|
309
|
+
};
|
|
310
|
+
type CreateCampaignInput = {
|
|
311
|
+
tenantId?: string;
|
|
312
|
+
name: string;
|
|
313
|
+
discount: DiscountPolicy;
|
|
314
|
+
eligibility?: EligibilityRules;
|
|
315
|
+
startsAt?: Date | null;
|
|
316
|
+
endsAt?: Date | null;
|
|
317
|
+
maxCoupons?: number | null;
|
|
318
|
+
metadata?: Record<string, unknown> | null;
|
|
319
|
+
actorId?: string;
|
|
320
|
+
};
|
|
321
|
+
type CampaignIssueArgs = {
|
|
322
|
+
tenantId?: string;
|
|
323
|
+
campaignId: string;
|
|
324
|
+
count: number;
|
|
325
|
+
codeLength?: number;
|
|
326
|
+
maxAttempts?: number;
|
|
327
|
+
actorId?: string;
|
|
328
|
+
};
|
|
329
|
+
type CampaignSummary = {
|
|
330
|
+
id: string;
|
|
331
|
+
tenantId: string;
|
|
332
|
+
name: string;
|
|
333
|
+
status: CampaignStatus;
|
|
334
|
+
issuedCount: number;
|
|
335
|
+
redeemedCount: number;
|
|
336
|
+
remaining: number | null;
|
|
337
|
+
totalDiscount: number;
|
|
338
|
+
uniqueUsers: number;
|
|
339
|
+
lastRedeemedAt: Date | null;
|
|
340
|
+
};
|
|
341
|
+
type CouponEvent = {
|
|
342
|
+
type: "coupon.created";
|
|
343
|
+
at: Date;
|
|
344
|
+
tenantId: string;
|
|
345
|
+
coupon: Coupon;
|
|
346
|
+
} | {
|
|
347
|
+
type: "coupon.issued";
|
|
348
|
+
at: Date;
|
|
349
|
+
tenantId: string;
|
|
350
|
+
issuance: Issuance;
|
|
351
|
+
} | {
|
|
352
|
+
type: "coupon.issued.bulk";
|
|
353
|
+
at: Date;
|
|
354
|
+
tenantId: string;
|
|
355
|
+
campaignId?: string;
|
|
356
|
+
count: number;
|
|
357
|
+
} | {
|
|
358
|
+
type: "coupon.redeemed";
|
|
359
|
+
at: Date;
|
|
360
|
+
tenantId: string;
|
|
361
|
+
redemption: Redemption;
|
|
362
|
+
} | {
|
|
363
|
+
type: "coupon.redeem.rejected";
|
|
364
|
+
at: Date;
|
|
365
|
+
tenantId: string;
|
|
366
|
+
code: string;
|
|
367
|
+
reason: CouponRejectReason;
|
|
368
|
+
userId?: string | null;
|
|
369
|
+
orderRef?: string | null;
|
|
370
|
+
} | {
|
|
371
|
+
type: "coupon.paused";
|
|
372
|
+
at: Date;
|
|
373
|
+
tenantId: string;
|
|
374
|
+
couponId: string;
|
|
375
|
+
} | {
|
|
376
|
+
type: "coupon.resumed";
|
|
377
|
+
at: Date;
|
|
378
|
+
tenantId: string;
|
|
379
|
+
couponId: string;
|
|
380
|
+
} | {
|
|
381
|
+
type: "coupon.archived";
|
|
382
|
+
at: Date;
|
|
383
|
+
tenantId: string;
|
|
384
|
+
couponId: string;
|
|
385
|
+
} | {
|
|
386
|
+
type: "campaign.created";
|
|
387
|
+
at: Date;
|
|
388
|
+
tenantId: string;
|
|
389
|
+
campaign: Campaign;
|
|
390
|
+
} | {
|
|
391
|
+
type: "campaign.closed";
|
|
392
|
+
at: Date;
|
|
393
|
+
tenantId: string;
|
|
394
|
+
campaignId: string;
|
|
395
|
+
};
|
|
396
|
+
interface CouponEventHandler {
|
|
397
|
+
onEvent(event: CouponEvent): void | Promise<void>;
|
|
398
|
+
}
|
|
399
|
+
interface CouponLogger {
|
|
400
|
+
debug(msg: string, meta?: Record<string, unknown>): void;
|
|
401
|
+
info(msg: string, meta?: Record<string, unknown>): void;
|
|
402
|
+
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
403
|
+
error(msg: string, meta?: Record<string, unknown>): void;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Minimal structural interface for the subset of PrismaClient methods we use.
|
|
407
|
+
* Consumers pass their own PrismaClient which will structurally match this.
|
|
408
|
+
*
|
|
409
|
+
* We deliberately AVOID importing any type from @prisma/client.
|
|
410
|
+
*/
|
|
411
|
+
type WhereInput = Record<string, unknown>;
|
|
412
|
+
type OrderByInput = Record<string, unknown> | Record<string, unknown>[];
|
|
413
|
+
interface FindFirstArgs {
|
|
414
|
+
where?: WhereInput;
|
|
415
|
+
orderBy?: OrderByInput;
|
|
416
|
+
select?: Record<string, unknown>;
|
|
417
|
+
include?: Record<string, unknown>;
|
|
418
|
+
}
|
|
419
|
+
interface FindUniqueArgs {
|
|
420
|
+
where: WhereInput;
|
|
421
|
+
select?: Record<string, unknown>;
|
|
422
|
+
include?: Record<string, unknown>;
|
|
423
|
+
}
|
|
424
|
+
interface FindManyArgs {
|
|
425
|
+
where?: WhereInput;
|
|
426
|
+
orderBy?: OrderByInput;
|
|
427
|
+
take?: number;
|
|
428
|
+
skip?: number;
|
|
429
|
+
cursor?: WhereInput;
|
|
430
|
+
select?: Record<string, unknown>;
|
|
431
|
+
include?: Record<string, unknown>;
|
|
432
|
+
}
|
|
433
|
+
interface CreateArgs {
|
|
434
|
+
data: Record<string, unknown>;
|
|
435
|
+
select?: Record<string, unknown>;
|
|
436
|
+
}
|
|
437
|
+
interface CreateManyArgs {
|
|
438
|
+
data: Record<string, unknown>[];
|
|
439
|
+
skipDuplicates?: boolean;
|
|
440
|
+
}
|
|
441
|
+
interface UpdateArgs {
|
|
442
|
+
where: WhereInput;
|
|
443
|
+
data: Record<string, unknown>;
|
|
444
|
+
select?: Record<string, unknown>;
|
|
445
|
+
}
|
|
446
|
+
interface UpdateManyArgs {
|
|
447
|
+
where: WhereInput;
|
|
448
|
+
data: Record<string, unknown>;
|
|
449
|
+
}
|
|
450
|
+
interface CountArgs {
|
|
451
|
+
where?: WhereInput;
|
|
452
|
+
}
|
|
453
|
+
interface AggregateArgs {
|
|
454
|
+
where?: WhereInput;
|
|
455
|
+
_sum?: Record<string, true>;
|
|
456
|
+
_count?: Record<string, true> | true;
|
|
457
|
+
_max?: Record<string, true>;
|
|
458
|
+
}
|
|
459
|
+
interface DelegateLike<TRow = Record<string, unknown>> {
|
|
460
|
+
findFirst(args?: FindFirstArgs): Promise<TRow | null>;
|
|
461
|
+
findUnique(args: FindUniqueArgs): Promise<TRow | null>;
|
|
462
|
+
findMany(args?: FindManyArgs): Promise<TRow[]>;
|
|
463
|
+
create(args: CreateArgs): Promise<TRow>;
|
|
464
|
+
createMany(args: CreateManyArgs): Promise<{
|
|
465
|
+
count: number;
|
|
466
|
+
}>;
|
|
467
|
+
update(args: UpdateArgs): Promise<TRow>;
|
|
468
|
+
updateMany(args: UpdateManyArgs): Promise<{
|
|
469
|
+
count: number;
|
|
470
|
+
}>;
|
|
471
|
+
count(args?: CountArgs): Promise<number>;
|
|
472
|
+
aggregate?(args: AggregateArgs): Promise<Record<string, unknown>>;
|
|
473
|
+
}
|
|
474
|
+
interface PrismaTxLike {
|
|
475
|
+
/**
|
|
476
|
+
* Delegates produced by the unmodified schema fragment.
|
|
477
|
+
*
|
|
478
|
+
* They are optional on purpose: a consumer who renamed the models to avoid
|
|
479
|
+
* a collision (and declared the mapping through `config.models`) still
|
|
480
|
+
* satisfies this interface. Delegates are never read off this type
|
|
481
|
+
* directly — `createDelegateResolver` looks them up by their configured
|
|
482
|
+
* name at call time and raises `InvalidModelNameError` when one is absent.
|
|
483
|
+
*/
|
|
484
|
+
coupon?: DelegateLike;
|
|
485
|
+
campaign?: DelegateLike;
|
|
486
|
+
couponIssuance?: DelegateLike;
|
|
487
|
+
couponRedemption?: DelegateLike;
|
|
488
|
+
couponAuditLog?: DelegateLike;
|
|
489
|
+
/**
|
|
490
|
+
* Parameterized raw query — used only for `SELECT ... FOR UPDATE` row locks
|
|
491
|
+
* in the redeem() concurrency path. Tagged-template style.
|
|
492
|
+
*/
|
|
493
|
+
$queryRaw<T = unknown>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T>;
|
|
494
|
+
}
|
|
495
|
+
interface PrismaLike extends PrismaTxLike {
|
|
496
|
+
$transaction<T>(fn: (tx: PrismaTxLike) => Promise<T>, options?: {
|
|
497
|
+
maxWait?: number;
|
|
498
|
+
timeout?: number;
|
|
499
|
+
isolationLevel?: string;
|
|
500
|
+
}): Promise<T>;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Prisma delegate / physical table resolution.
|
|
505
|
+
*
|
|
506
|
+
* The schema fragment ships five models (`Coupon`, `Campaign`,
|
|
507
|
+
* `CouponIssuance`, `CouponRedemption`, `CouponAuditLog`). A consumer whose
|
|
508
|
+
* own schema already owns one of those names — `Campaign` is the common
|
|
509
|
+
* collision — can rename them and declare the mapping through
|
|
510
|
+
* `createCouponClient({ models, tables })`.
|
|
511
|
+
*
|
|
512
|
+
* Every delegate access and every raw-SQL table reference in this package is
|
|
513
|
+
* routed through the resolver built here, so no model name is hardcoded
|
|
514
|
+
* anywhere else in `src/`.
|
|
515
|
+
*/
|
|
516
|
+
|
|
517
|
+
/** The five logical models this package owns. */
|
|
518
|
+
type CouponModelKey = "coupon" | "campaign" | "couponIssuance" | "couponRedemption" | "couponAuditLog";
|
|
519
|
+
/** Prisma delegate property names, keyed by logical model. */
|
|
520
|
+
type ModelNameMap = Record<CouponModelKey, string>;
|
|
521
|
+
/** Physical table names (as they appear in SQL), keyed by logical model. */
|
|
522
|
+
type TableNameMap = Record<CouponModelKey, string>;
|
|
523
|
+
/** Delegate names produced by the unmodified schema fragment. */
|
|
524
|
+
declare const DEFAULT_MODEL_NAMES: ModelNameMap;
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* createCouponClient — factory for the coupon domain client.
|
|
528
|
+
*
|
|
529
|
+
* Implements spec.md §4 public API, concurrency algorithm (§9.3),
|
|
530
|
+
* multi-tenant guards, audit logging, and pluggable event emission.
|
|
531
|
+
*/
|
|
532
|
+
|
|
533
|
+
type CouponClientConfig = {
|
|
534
|
+
prisma: PrismaLike;
|
|
535
|
+
defaultTenantId?: string;
|
|
536
|
+
idGenerator?: () => string;
|
|
537
|
+
codeGenerator?: (opts: {
|
|
538
|
+
length: number;
|
|
539
|
+
alphabet?: string;
|
|
540
|
+
}) => string;
|
|
541
|
+
events?: CouponEventHandler;
|
|
542
|
+
now?: () => Date;
|
|
543
|
+
logger?: CouponLogger;
|
|
544
|
+
/**
|
|
545
|
+
* Prisma delegate names, when the schema fragment's models were renamed to
|
|
546
|
+
* avoid a collision with the consumer's own schema. Defaults to
|
|
547
|
+
* `{ coupon, campaign, couponIssuance, couponRedemption, couponAuditLog }`.
|
|
548
|
+
*
|
|
549
|
+
* @example { campaign: "couponCampaign" }
|
|
550
|
+
*/
|
|
551
|
+
models?: Partial<ModelNameMap>;
|
|
552
|
+
/**
|
|
553
|
+
* Physical table names, used by the `SELECT ... FOR UPDATE` row lock in
|
|
554
|
+
* `redeem()`. Defaults to each delegate name capitalized, which is Prisma's
|
|
555
|
+
* own convention; pass this only when the schema uses `@@map`.
|
|
556
|
+
*
|
|
557
|
+
* @example { coupon: "coupons" }
|
|
558
|
+
*/
|
|
559
|
+
tables?: Partial<TableNameMap>;
|
|
560
|
+
};
|
|
561
|
+
interface CouponClient {
|
|
562
|
+
coupons: {
|
|
563
|
+
create(input: CreateCouponInput): Promise<Coupon>;
|
|
564
|
+
issue(input: IssueCouponInput): Promise<Issuance>;
|
|
565
|
+
issueBulk(input: IssueBulkInput): Promise<BulkIssuanceResult>;
|
|
566
|
+
findByCode(args: {
|
|
567
|
+
tenantId?: string;
|
|
568
|
+
code: string;
|
|
569
|
+
}): Promise<Coupon | null>;
|
|
570
|
+
get(args: {
|
|
571
|
+
tenantId?: string;
|
|
572
|
+
id: string;
|
|
573
|
+
}): Promise<Coupon | null>;
|
|
574
|
+
list(args: ListCouponsArgs): Promise<Paged<Coupon>>;
|
|
575
|
+
pause(args: {
|
|
576
|
+
tenantId?: string;
|
|
577
|
+
id: string;
|
|
578
|
+
actorId?: string;
|
|
579
|
+
}): Promise<Coupon>;
|
|
580
|
+
resume(args: {
|
|
581
|
+
tenantId?: string;
|
|
582
|
+
id: string;
|
|
583
|
+
actorId?: string;
|
|
584
|
+
}): Promise<Coupon>;
|
|
585
|
+
archive(args: {
|
|
586
|
+
tenantId?: string;
|
|
587
|
+
id: string;
|
|
588
|
+
actorId?: string;
|
|
589
|
+
}): Promise<Coupon>;
|
|
590
|
+
};
|
|
591
|
+
validate(input: ValidateInput): Promise<ValidationResult>;
|
|
592
|
+
redeem(input: RedeemInput): Promise<RedemptionResult>;
|
|
593
|
+
discount: {
|
|
594
|
+
calculate(input: DiscountInput): DiscountBreakdown;
|
|
595
|
+
};
|
|
596
|
+
campaigns: {
|
|
597
|
+
create(input: CreateCampaignInput): Promise<Campaign>;
|
|
598
|
+
get(args: {
|
|
599
|
+
tenantId?: string;
|
|
600
|
+
id: string;
|
|
601
|
+
}): Promise<Campaign | null>;
|
|
602
|
+
list(args: ListCampaignsArgs): Promise<Paged<Campaign>>;
|
|
603
|
+
issue(args: CampaignIssueArgs): Promise<BulkIssuanceResult>;
|
|
604
|
+
summary(args: {
|
|
605
|
+
tenantId?: string;
|
|
606
|
+
id: string;
|
|
607
|
+
}): Promise<CampaignSummary>;
|
|
608
|
+
close(args: {
|
|
609
|
+
tenantId?: string;
|
|
610
|
+
id: string;
|
|
611
|
+
actorId?: string;
|
|
612
|
+
}): Promise<Campaign>;
|
|
613
|
+
};
|
|
614
|
+
audit: {
|
|
615
|
+
listIssuances(args: ListIssuancesArgs): Promise<Paged<Issuance>>;
|
|
616
|
+
listRedemptions(args: ListRedemptionsArgs): Promise<Paged<Redemption>>;
|
|
617
|
+
listLogs(args: ListAuditLogsArgs): Promise<Paged<AuditLog>>;
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
declare function createCouponClient(config: CouponClientConfig): CouponClient;
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Pure discount calculation engine.
|
|
624
|
+
*
|
|
625
|
+
* [N] NO side effects, NO I/O, NO time dependency.
|
|
626
|
+
* The only exceptions it throws are validation-level CouponError subclasses
|
|
627
|
+
* that correspond to the input itself (not to coupon state).
|
|
628
|
+
*/
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Validates the shape of a DiscountPolicy. Throws InvalidDiscountPolicyError
|
|
632
|
+
* on any violation. Public so it can be re-used by validators.
|
|
633
|
+
*/
|
|
634
|
+
declare function validateDiscountPolicy(policy: DiscountPolicy): void;
|
|
635
|
+
/**
|
|
636
|
+
* Pure discount calculation. Returns a DiscountBreakdown with total >= 0.
|
|
637
|
+
* Throws CurrencyMismatchError if input.currency != policy.currency (when policy carries one).
|
|
638
|
+
* Throws MinimumOrderNotMetError if subtotal < policy.minOrder.
|
|
639
|
+
*/
|
|
640
|
+
declare function calculate(input: DiscountInput): DiscountBreakdown;
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Error taxonomy for @withwiz/coupon.
|
|
644
|
+
*
|
|
645
|
+
* All errors extend CouponError. Each concrete subclass exposes a static
|
|
646
|
+
* `code` string that matches the corresponding CouponRejectReason value
|
|
647
|
+
* (where applicable — see sprint_contract.md §7.4).
|
|
648
|
+
*
|
|
649
|
+
* All messages are in English. i18n is a consumer concern.
|
|
650
|
+
*/
|
|
651
|
+
interface CouponErrorContext {
|
|
652
|
+
tenantId?: string;
|
|
653
|
+
couponId?: string;
|
|
654
|
+
couponCode?: string;
|
|
655
|
+
cause?: unknown;
|
|
656
|
+
}
|
|
657
|
+
declare abstract class CouponError extends Error {
|
|
658
|
+
readonly name: string;
|
|
659
|
+
readonly code: string;
|
|
660
|
+
readonly tenantId?: string;
|
|
661
|
+
readonly couponId?: string;
|
|
662
|
+
readonly couponCode?: string;
|
|
663
|
+
readonly cause?: unknown;
|
|
664
|
+
protected constructor(message: string, code: string, ctx?: CouponErrorContext);
|
|
665
|
+
}
|
|
666
|
+
declare class ValidationError extends CouponError {
|
|
667
|
+
static readonly code = "VALIDATION_ERROR";
|
|
668
|
+
readonly details?: unknown;
|
|
669
|
+
constructor(message: string, details?: unknown, ctx?: CouponErrorContext);
|
|
670
|
+
}
|
|
671
|
+
declare class InvalidDiscountPolicyError extends CouponError {
|
|
672
|
+
static readonly code = "INVALID_DISCOUNT_POLICY";
|
|
673
|
+
constructor(message: string, ctx?: CouponErrorContext);
|
|
674
|
+
}
|
|
675
|
+
declare class InvalidDateRangeError extends CouponError {
|
|
676
|
+
static readonly code = "INVALID_DATE_RANGE";
|
|
677
|
+
constructor(message: string, ctx?: CouponErrorContext);
|
|
678
|
+
}
|
|
679
|
+
declare class CurrencyMismatchError extends CouponError {
|
|
680
|
+
static readonly code = "CURRENCY_MISMATCH";
|
|
681
|
+
constructor(message: string, ctx?: CouponErrorContext);
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Raised when `config.models` / `config.tables` name something the client
|
|
685
|
+
* cannot resolve, or when a name is not a plain SQL identifier. This is a
|
|
686
|
+
* wiring mistake, not an end-user input error: a malformed name is rejected
|
|
687
|
+
* while the client is being created, and a name that resolves to no delegate
|
|
688
|
+
* is rejected on the first call that touches it.
|
|
689
|
+
*/
|
|
690
|
+
declare class InvalidModelNameError extends CouponError {
|
|
691
|
+
static readonly code = "INVALID_MODEL_NAME";
|
|
692
|
+
constructor(message: string, ctx?: CouponErrorContext);
|
|
693
|
+
}
|
|
694
|
+
declare class CouponNotFoundError extends CouponError {
|
|
695
|
+
static readonly code = "NOT_FOUND";
|
|
696
|
+
constructor(message?: string, ctx?: CouponErrorContext);
|
|
697
|
+
}
|
|
698
|
+
declare class CouponArchivedError extends CouponError {
|
|
699
|
+
static readonly code = "ARCHIVED";
|
|
700
|
+
constructor(message?: string, ctx?: CouponErrorContext);
|
|
701
|
+
}
|
|
702
|
+
declare class CouponInactiveError extends CouponError {
|
|
703
|
+
static readonly code = "PAUSED";
|
|
704
|
+
constructor(message?: string, ctx?: CouponErrorContext);
|
|
705
|
+
}
|
|
706
|
+
declare class CouponExpiredError extends CouponError {
|
|
707
|
+
static readonly code = "EXPIRED";
|
|
708
|
+
constructor(message?: string, ctx?: CouponErrorContext, codeOverride?: "EXPIRED" | "NOT_STARTED");
|
|
709
|
+
}
|
|
710
|
+
declare class CouponExhaustedError extends CouponError {
|
|
711
|
+
static readonly code = "EXHAUSTED";
|
|
712
|
+
constructor(message?: string, ctx?: CouponErrorContext);
|
|
713
|
+
}
|
|
714
|
+
declare class CouponAlreadyRedeemedError extends CouponError {
|
|
715
|
+
static readonly code = "ALREADY_REDEEMED";
|
|
716
|
+
constructor(message?: string, ctx?: CouponErrorContext);
|
|
717
|
+
}
|
|
718
|
+
declare class PerUserLimitExceededError extends CouponError {
|
|
719
|
+
static readonly code = "PER_USER_LIMIT";
|
|
720
|
+
constructor(message?: string, ctx?: CouponErrorContext);
|
|
721
|
+
}
|
|
722
|
+
declare class IneligibleUserError extends CouponError {
|
|
723
|
+
static readonly code = "INELIGIBLE_USER";
|
|
724
|
+
constructor(message?: string, ctx?: CouponErrorContext);
|
|
725
|
+
}
|
|
726
|
+
declare class IneligiblePlanError extends CouponError {
|
|
727
|
+
static readonly code = "INELIGIBLE_PLAN";
|
|
728
|
+
constructor(message?: string, ctx?: CouponErrorContext);
|
|
729
|
+
}
|
|
730
|
+
declare class IneligibleProductError extends CouponError {
|
|
731
|
+
static readonly code = "INELIGIBLE_PRODUCT";
|
|
732
|
+
constructor(message?: string, ctx?: CouponErrorContext);
|
|
733
|
+
}
|
|
734
|
+
declare class IneligibleCategoryError extends CouponError {
|
|
735
|
+
static readonly code = "INELIGIBLE_CATEGORY";
|
|
736
|
+
constructor(message?: string, ctx?: CouponErrorContext);
|
|
737
|
+
}
|
|
738
|
+
declare class MinimumOrderNotMetError extends CouponError {
|
|
739
|
+
static readonly code = "MIN_ORDER_NOT_MET";
|
|
740
|
+
constructor(message?: string, ctx?: CouponErrorContext);
|
|
741
|
+
}
|
|
742
|
+
declare class CampaignClosedError extends CouponError {
|
|
743
|
+
static readonly code = "CAMPAIGN_CLOSED";
|
|
744
|
+
constructor(message?: string, ctx?: CouponErrorContext);
|
|
745
|
+
}
|
|
746
|
+
declare class CampaignCapacityExceededError extends CouponError {
|
|
747
|
+
static readonly code = "CAMPAIGN_CAPACITY_EXCEEDED";
|
|
748
|
+
constructor(message?: string, ctx?: CouponErrorContext);
|
|
749
|
+
}
|
|
750
|
+
declare class TenantMismatchError extends CouponError {
|
|
751
|
+
static readonly code = "TENANT_MISMATCH";
|
|
752
|
+
constructor(message?: string, ctx?: CouponErrorContext);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
export { AUDIT_ACTION, type AuditAction, type AuditLog, type BulkIssuanceResult, CAMPAIGN_STATUS, COUPON_REJECT_REASON, COUPON_STATUS, type Campaign, CampaignCapacityExceededError, CampaignClosedError, type CampaignIssueArgs, type CampaignStatus, type CampaignSummary, type Coupon, CouponAlreadyRedeemedError, CouponArchivedError, type CouponClient, type CouponClientConfig, CouponError, type CouponEvent, type CouponEventHandler, CouponExhaustedError, CouponExpiredError, CouponInactiveError, type CouponLogger, type CouponModelKey, CouponNotFoundError, type CouponRejectReason, type CouponStatus, type CreateCampaignInput, type CreateCouponInput, CurrencyMismatchError, DEFAULT_MODEL_NAMES, type DiscountBreakdown, type DiscountInput, type DiscountPolicy, type EligibilityRules, IneligibleCategoryError, IneligiblePlanError, IneligibleProductError, IneligibleUserError, InvalidDateRangeError, InvalidDiscountPolicyError, InvalidModelNameError, type Issuance, type IssueBulkInput, type IssueCouponInput, LIMITS, type ListAuditLogsArgs, type ListCampaignsArgs, type ListCouponsArgs, type ListIssuancesArgs, type ListRedemptionsArgs, MinimumOrderNotMetError, type ModelNameMap, type Paged, PerUserLimitExceededError, type PrismaLike, type PrismaTxLike, type RedeemInput, type Redemption, type RedemptionResult, type TableNameMap, TenantMismatchError, type ValidateContext, type ValidateInput, ValidationError, type ValidationResult, calculate as calculateDiscount, createCouponClient, validateDiscountPolicy };
|