@krizaka/coupons 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,172 @@
1
+ import { p as CouponSettings, a as AuditPort, F as RateLimitPort, s as CreditsPort, y as OutboxPort, C as CouponStore, A as Actor, N as NewCoupon, j as Coupon, n as CouponPatch, m as CouponFilter, H as RedemptionFilter, G as Redemption, z as Partner, B as PartnerRow, u as DiscountTerms, t as DiscountEffect, i as CheckoutContext, r as CreditEffect, o as CouponRefusal } from './rules-CCd1HhOA.js';
2
+ import { Clock, Query } from '@krizaka/db';
3
+
4
+ interface CouponsDeps {
5
+ clock: Clock;
6
+ /** The settings, read at each call so the admin's change applies at once: an object, or a function over the settings service. */
7
+ settings: CouponSettings | (() => CouponSettings | Promise<CouponSettings>);
8
+ /** The trail (`createAudit(...)` of `@krizaka/audit`): every admin change and every redemption is recorded in the caller's transaction. */
9
+ audit: AuditPort;
10
+ /** The limiter (`createLimiter(...)` of `@krizaka/rate-limit`): each attempt with a code counts one hit on `COUPON_ATTEMPT_ACTION`. */
11
+ rateLimit: RateLimitPort;
12
+ /** The credits ledger (`createCredits(...)` of `@krizaka/credits`): needed for credit coupons. */
13
+ credits?: CreditsPort;
14
+ /** The outbox (`createOutbox(...)` of `@krizaka/outbox`): the events are appended in the caller's transaction. */
15
+ outbox?: OutboxPort;
16
+ /** The store over a query (a pool or a transaction). Default: the PostgreSQL store on schema `coupons`. */
17
+ storeFor?: (query: Query) => CouponStore;
18
+ /** The Postgres schema, when the product renamed the module's (used by the default store). */
19
+ schema?: string;
20
+ /** Fresh ids; default random UUIDs. */
21
+ newId?: () => string;
22
+ }
23
+ /** The system as an actor (a webhook, a sweeper). */
24
+ declare const SYSTEM: Actor;
25
+ /** What the flows and the admin share: the clock, the settings, the store, the trail and the outbox. */
26
+ interface Kernel {
27
+ deps: CouponsDeps;
28
+ now(): Date;
29
+ settings(): Promise<CouponSettings>;
30
+ store(query: Query): CouponStore;
31
+ newId(): string;
32
+ record(tx: Query, actor: Actor, action: string, target: {
33
+ type: string;
34
+ id: string;
35
+ }, detail?: Record<string, unknown>): Promise<void>;
36
+ announce(tx: Query, type: string, seed: string, payload: Record<string, unknown>): Promise<void>;
37
+ }
38
+
39
+ interface PartnerInput {
40
+ displayName: string;
41
+ contactEmail?: string | null;
42
+ }
43
+ /** The admin's side (ADR-002 §6): coupons and partners, every change audited in the caller's transaction. */
44
+ declare function makeAdmin(k: Kernel): {
45
+ generateCode: (query: Query) => Promise<string>;
46
+ create: (tx: Query, actor: Actor, input: NewCoupon) => Promise<Coupon>;
47
+ update: (tx: Query, actor: Actor, id: string, patch: CouponPatch) => Promise<Coupon>;
48
+ pause: (tx: Query, actor: Actor, id: string) => Promise<Coupon>;
49
+ resume: (tx: Query, actor: Actor, id: string) => Promise<Coupon>;
50
+ archive: (tx: Query, actor: Actor, id: string) => Promise<Coupon>;
51
+ get: (query: Query, id: string) => Promise<Coupon | null>;
52
+ list: (query: Query, filter?: Partial<CouponFilter>) => Promise<Coupon[]>;
53
+ redemptions: (query: Query, filter?: Partial<RedemptionFilter>) => Promise<Redemption[]>;
54
+ checkPrices: (tx: Query, prices: readonly {
55
+ plan: string;
56
+ amountMinor: number;
57
+ currency: string;
58
+ }[]) => Promise<string[]>;
59
+ partners: {
60
+ create: (tx: Query, actor: Actor, input: PartnerInput) => Promise<Partner>;
61
+ archive: (tx: Query, actor: Actor, id: string) => Promise<Partner>;
62
+ restore: (tx: Query, actor: Actor, id: string) => Promise<Partner>;
63
+ erase: (tx: Query, actor: Actor, id: string) => Promise<Partner>;
64
+ get: (query: Query, id: string) => Promise<Partner | null>;
65
+ list: (query: Query, options?: {
66
+ includeArchived?: boolean;
67
+ limit?: number;
68
+ offset?: number;
69
+ }) => Promise<Partner[]>;
70
+ export: (query: Query, id: string) => Promise<{
71
+ partner: Partner;
72
+ rows: PartnerRow[];
73
+ }>;
74
+ report: (tx: Query, actor: Actor, id: string, period?: {
75
+ from?: Date;
76
+ to?: Date;
77
+ }) => Promise<PartnerRow[]>;
78
+ };
79
+ };
80
+ type CouponAdmin = ReturnType<typeof makeAdmin>;
81
+
82
+ /** The coupon as a checkout shows it. Never the limits or the partner. */
83
+ interface CouponView {
84
+ id: string;
85
+ code: string;
86
+ kind: Coupon["kind"];
87
+ purpose: Coupon["purpose"];
88
+ endsAt: Date | null;
89
+ }
90
+ type Validation = {
91
+ ok: true;
92
+ coupon: CouponView;
93
+ effect: DiscountEffect | CreditEffect;
94
+ } | {
95
+ ok: false;
96
+ code: CouponRefusal;
97
+ retryAfterSeconds?: number;
98
+ };
99
+ interface ApplyInput extends Omit<CheckoutContext, "mode"> {
100
+ code: string;
101
+ /** One key per checkout attempt: a replay returns the same reservation. */
102
+ idempotencyKey: string;
103
+ }
104
+ interface Applied {
105
+ redemptionId: string;
106
+ /** The immutable terms for the payments port's `checkout({ discount })`. */
107
+ discount: DiscountTerms;
108
+ effect: DiscountEffect;
109
+ reservedUntil: Date;
110
+ replayed: boolean;
111
+ }
112
+ interface RedeemInput {
113
+ /** From the checkout's metadata (`redemption_id`); found first. */
114
+ redemptionId?: string | null;
115
+ /** The checkout session's reference; found when the metadata is missing. */
116
+ checkoutRef?: string | null;
117
+ /** The payment (a payment intent), or the invoice for a subscription; null for a free checkout. */
118
+ paymentRef: string | null;
119
+ amountPaidMinor: number | null;
120
+ amountDiscountMinor: number;
121
+ currency: string | null;
122
+ }
123
+ interface CreditRedemption {
124
+ redemptionId: string;
125
+ grantId: string;
126
+ credits: number;
127
+ expiresAt: Date;
128
+ replayed: boolean;
129
+ }
130
+
131
+ /** The coupons of a product (ADR-002 §5.1). Every function takes the `Query` it works through: pass `tx` to join a unit of work. */
132
+ interface Coupons {
133
+ /** Read-only: what a code would give, or why not. Counts one attempt on the limiter, before any lookup. Never reserves. */
134
+ validate(query: Query, code: string, context: CheckoutContext): Promise<Validation>;
135
+ /** Reserves a use of a discount code for a checkout (a short transaction: no network inside), idempotent by key. */
136
+ apply(tx: Query, input: ApplyInput): Promise<Applied>;
137
+ /** Stores the provider's session on the reservation and makes it outlive the session. */
138
+ attachCheckout(tx: Query, redemptionId: string, checkoutRef: string, sessionExpiresAt?: Date | null): Promise<void>;
139
+ /** On the payment event, in its transaction: confirms the use once. A payment after the release is confirmed anyway (`overLimit`). */
140
+ redeem(tx: Query, input: RedeemInput): Promise<"confirmed" | "already" | "unknown">;
141
+ /** Frees an unpaid reservation (a failed session, an expired checkout). `false` when there was none to free. */
142
+ release(tx: Query, ref: {
143
+ redemptionId?: string;
144
+ checkoutRef?: string;
145
+ }): Promise<boolean>;
146
+ /** The sweeper: frees the reservations past their deadline. */
147
+ releaseExpired(tx: Query): Promise<number>;
148
+ /** A refund (cumulative amount). Gives the use back only on a full refund with `coupons.refundRestoresUse`. */
149
+ refund(tx: Query, input: {
150
+ paymentRef?: string | null;
151
+ invoiceRef?: string | null;
152
+ amountRefunded: number;
153
+ }): Promise<"refunded" | "already" | "unknown">;
154
+ /** Redeems a credit code: the same rules, then a capped grant in the credits ledger, in the same transaction. Idempotent by key. */
155
+ redeemCredit(tx: Query, input: {
156
+ code: string;
157
+ accountId: string;
158
+ idempotencyKey: string;
159
+ ip?: string | null;
160
+ hasPaidBefore?: boolean;
161
+ }): Promise<CreditRedemption>;
162
+ /** The admin revokes a credit redemption: what is left of its grant is written off; credits already spent are never taken back. */
163
+ revoke(tx: Query, actor: Actor, redemptionId: string): Promise<void>;
164
+ admin: CouponAdmin;
165
+ }
166
+ /**
167
+ * The coupons of a product.
168
+ * @param deps the clock, the settings, the trail, the limiter, and optionally the credits ledger, the outbox, the store
169
+ */
170
+ declare function createCoupons(deps: CouponsDeps): Coupons;
171
+
172
+ export { type Applied as A, type CouponAdmin as C, type PartnerInput as P, type RedeemInput as R, SYSTEM as S, type Validation as V, type ApplyInput as a, type CouponView as b, type Coupons as c, type CouponsDeps as d, type CreditRedemption as e, createCoupons as f };
@@ -0,0 +1,135 @@
1
+ export { A as Applied, a as ApplyInput, C as CouponAdmin, b as CouponView, c as Coupons, d as CouponsDeps, e as CreditRedemption, P as PartnerInput, R as RedeemInput, S as SYSTEM, V as Validation, f as createCoupons } from './coupons-BF-3HFdw.js';
2
+ import { C as CouponStore } from './rules-CCd1HhOA.js';
3
+ export { A as Actor, a as AuditPort, b as COUPON_ATTEMPT_ACTION, c as COUPON_BOUNDS, d as COUPON_CODE, e as COUPON_DEFAULTS, f as COUPON_ERROR, g as COUPON_STATUS, h as COUPON_STATUS_CODE, i as CheckoutContext, j as Coupon, k as CouponError, l as CouponErrorCode, m as CouponFilter, n as CouponPatch, o as CouponRefusal, p as CouponSettings, q as CouponStatus, r as CreditEffect, s as CreditsPort, D as DURATION, t as DiscountEffect, u as DiscountTerms, v as Duration, K as KIND, w as Kind, N as NewCoupon, O as ON_EXHAUSTED, x as OnExhausted, y as OutboxPort, P as PURPOSE, z as Partner, B as PartnerRow, E as Purpose, R as REDEMPTION_STATUS, F as RateLimitPort, G as Redemption, H as RedemptionFilter, I as RedemptionPatch, J as RedemptionStatus, U as Usage, L as countsAsUse, M as effectOf, Q as generateCode, S as normalizeCode, T as refusalOf, V as termsOf } from './rules-CCd1HhOA.js';
4
+ import * as _krizaka_db from '@krizaka/db';
5
+ import { Query } from '@krizaka/db';
6
+ export { Clock, Query } from '@krizaka/db';
7
+
8
+ /** The topic the coupons' events are appended to on the outbox. */
9
+ declare const COUPONS_TOPIC = "coupons";
10
+ declare const COUPON_EVENT: {
11
+ readonly redeemed: "evt.coupon.redeemed.v1";
12
+ readonly released: "evt.coupon.released.v1";
13
+ readonly refunded: "evt.coupon.refunded.v1";
14
+ readonly exhausted: "evt.coupon.exhausted.v1";
15
+ };
16
+ /** A stable UUID (version 5 shaped) from a seed: the same fact announced twice is one outbox message. */
17
+ declare function uuidOf(seed: string): string;
18
+ /** The JSON Schema of each event's payload. `evt.coupon.redeemed.v1` is the hook of the later commissions module: it carries the partner and the amounts. */
19
+ declare const COUPON_EVENT_SCHEMAS: {
20
+ readonly "evt.coupon.redeemed.v1": {
21
+ readonly type: "object";
22
+ readonly additionalProperties: false;
23
+ readonly required: readonly ["redemptionId", "couponId", "kind", "purpose", "partnerId", "amountPaidMinor", "amountDiscountMinor", "currency", "credits", "overLimit"];
24
+ readonly properties: {
25
+ readonly redemptionId: {
26
+ readonly type: "string";
27
+ };
28
+ readonly couponId: {
29
+ readonly type: "string";
30
+ };
31
+ readonly kind: {
32
+ readonly enum: readonly ["discount", "credit"];
33
+ };
34
+ readonly purpose: {
35
+ readonly enum: readonly ["partner", "promo", "demo", "support"];
36
+ };
37
+ readonly partnerId: {
38
+ readonly type: readonly ["string", "null"];
39
+ };
40
+ readonly amountPaidMinor: {
41
+ readonly type: readonly ["integer", "null"];
42
+ };
43
+ readonly amountDiscountMinor: {
44
+ readonly type: readonly ["integer", "null"];
45
+ };
46
+ readonly currency: {
47
+ readonly type: readonly ["string", "null"];
48
+ };
49
+ readonly credits: {
50
+ readonly type: readonly ["integer", "null"];
51
+ };
52
+ readonly overLimit: {
53
+ readonly type: "boolean";
54
+ };
55
+ };
56
+ };
57
+ readonly "evt.coupon.released.v1": {
58
+ readonly type: "object";
59
+ readonly additionalProperties: false;
60
+ readonly required: readonly ["redemptionId", "couponId"];
61
+ readonly properties: {
62
+ readonly redemptionId: {
63
+ readonly type: "string";
64
+ };
65
+ readonly couponId: {
66
+ readonly type: "string";
67
+ };
68
+ };
69
+ };
70
+ readonly "evt.coupon.refunded.v1": {
71
+ readonly type: "object";
72
+ readonly additionalProperties: false;
73
+ readonly required: readonly ["redemptionId", "couponId", "partnerId", "amountRefundedMinor", "currency", "useRestored"];
74
+ readonly properties: {
75
+ readonly redemptionId: {
76
+ readonly type: "string";
77
+ };
78
+ readonly couponId: {
79
+ readonly type: "string";
80
+ };
81
+ readonly partnerId: {
82
+ readonly type: readonly ["string", "null"];
83
+ };
84
+ readonly amountRefundedMinor: {
85
+ readonly type: "integer";
86
+ readonly minimum: 0;
87
+ };
88
+ readonly currency: {
89
+ readonly type: readonly ["string", "null"];
90
+ };
91
+ readonly useRestored: {
92
+ readonly type: "boolean";
93
+ };
94
+ };
95
+ };
96
+ readonly "evt.coupon.exhausted.v1": {
97
+ readonly type: "object";
98
+ readonly additionalProperties: false;
99
+ readonly required: readonly ["couponId", "partnerId"];
100
+ readonly properties: {
101
+ readonly couponId: {
102
+ readonly type: "string";
103
+ };
104
+ readonly partnerId: {
105
+ readonly type: readonly ["string", "null"];
106
+ };
107
+ };
108
+ };
109
+ };
110
+
111
+ /**
112
+ * An in-memory store, for unit tests and demos: the same behaviour as the database's (the contract kit runs on both) except that
113
+ * `forUpdate` locks nothing, because it has no transaction to hold a lock in. Concurrency is proved on PostgreSQL.
114
+ */
115
+ declare function memoryCouponStore(): CouponStore;
116
+
117
+ /**
118
+ * The PostgreSQL store of coupons, in schema `coupons` (see `couponsSchema`), over the `Query` it is given: the caller's
119
+ * transaction, so the work commits or rolls back with the product's. `couponByKey(…, true)` is `SELECT … FOR UPDATE`: the last use of a
120
+ * code is taken by one request at a time. The guarded transition of `updateRedemption` is one `UPDATE … WHERE status = ANY(…)`.
121
+ * Times come from the caller (the injected clock), never from the database's `now()`.
122
+ * @param schema the Postgres schema of the module, default `coupons`
123
+ */
124
+ declare function dbCouponStore(query: Query, schema?: string): CouponStore;
125
+
126
+ /**
127
+ * The tables of « coupons » (ADR-002), in their own Postgres schema: `partner` (a person who sells for us: a name and an optional
128
+ * e-mail, erasable), `coupon` (the code, its window, its limits and its terms: a discount or capped credits) and `redemption` (a use:
129
+ * reserved, confirmed, released or revoked, with the amounts and the provider's references). The one product table referenced is the
130
+ * owner (`owner`, the account table): a deleted account leaves its redemptions (`ON DELETE SET NULL`), because uses and revenue stay.
131
+ * Version 1 is frozen once released: a change is version 2.
132
+ */
133
+ declare const couponsSchema: _krizaka_db.ModuleSchema;
134
+
135
+ export { COUPONS_TOPIC, COUPON_EVENT, COUPON_EVENT_SCHEMAS, CouponStore, couponsSchema, dbCouponStore, memoryCouponStore, uuidOf };
package/dist/index.js ADDED
@@ -0,0 +1,63 @@
1
+ import {
2
+ COUPONS_TOPIC,
3
+ COUPON_EVENT,
4
+ COUPON_EVENT_SCHEMAS,
5
+ SYSTEM,
6
+ couponsSchema,
7
+ createCoupons,
8
+ dbCouponStore,
9
+ memoryCouponStore,
10
+ uuidOf
11
+ } from "./chunk-ECCGFGSV.js";
12
+ import {
13
+ COUPON_ATTEMPT_ACTION,
14
+ COUPON_BOUNDS,
15
+ COUPON_CODE,
16
+ COUPON_DEFAULTS,
17
+ COUPON_ERROR,
18
+ COUPON_STATUS,
19
+ COUPON_STATUS_CODE,
20
+ CouponError,
21
+ DURATION,
22
+ KIND,
23
+ ON_EXHAUSTED,
24
+ PURPOSE,
25
+ REDEMPTION_STATUS,
26
+ countsAsUse,
27
+ effectOf,
28
+ generateCode,
29
+ normalizeCode,
30
+ refusalOf,
31
+ termsOf
32
+ } from "./chunk-JMQO6PTK.js";
33
+ export {
34
+ COUPONS_TOPIC,
35
+ COUPON_ATTEMPT_ACTION,
36
+ COUPON_BOUNDS,
37
+ COUPON_CODE,
38
+ COUPON_DEFAULTS,
39
+ COUPON_ERROR,
40
+ COUPON_EVENT,
41
+ COUPON_EVENT_SCHEMAS,
42
+ COUPON_STATUS,
43
+ COUPON_STATUS_CODE,
44
+ CouponError,
45
+ DURATION,
46
+ KIND,
47
+ ON_EXHAUSTED,
48
+ PURPOSE,
49
+ REDEMPTION_STATUS,
50
+ SYSTEM,
51
+ countsAsUse,
52
+ couponsSchema,
53
+ createCoupons,
54
+ dbCouponStore,
55
+ effectOf,
56
+ generateCode,
57
+ memoryCouponStore,
58
+ normalizeCode,
59
+ refusalOf,
60
+ termsOf,
61
+ uuidOf
62
+ };
63
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}