@forklaunch/implementation-billing-stripe 1.2.12 → 1.3.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/lib/domain/schemas/billingPortal.schema.d.ts +2 -2
- package/lib/domain/schemas/index.js +27 -14
- package/lib/domain/schemas/index.mjs +33 -14
- package/lib/domain/schemas/plan.schema.d.ts +2 -2
- package/lib/domain/schemas/typebox/billingPortal.schema.d.ts +2 -2
- package/lib/domain/schemas/typebox/checkoutSession.schema.d.ts +3 -3
- package/lib/domain/schemas/typebox/checkoutSession.schema.d.ts.map +1 -1
- package/lib/domain/schemas/typebox/paymentLink.schema.d.ts +2 -1
- package/lib/domain/schemas/typebox/paymentLink.schema.d.ts.map +1 -1
- package/lib/domain/schemas/typebox/plan.schema.d.ts +2 -2
- package/lib/domain/schemas/typebox/subscription.schema.d.ts +1 -1
- package/lib/domain/schemas/zod/billingPortal.schema.d.ts +2 -2
- package/lib/domain/schemas/zod/checkoutSession.schema.d.ts +3 -3
- package/lib/domain/schemas/zod/checkoutSession.schema.d.ts.map +1 -1
- package/lib/domain/schemas/zod/paymentLink.schema.d.ts.map +1 -1
- package/lib/domain/schemas/zod/plan.schema.d.ts +2 -2
- package/lib/domain/schemas/zod/subscription.schema.d.ts +1 -1
- package/lib/domain/schemas/zod/subscription.schema.d.ts.map +1 -1
- package/lib/domain/types/billingPortal.mapper.types.d.ts +8 -0
- package/lib/domain/types/billingPortal.mapper.types.d.ts.map +1 -1
- package/lib/domain/types/checkoutSession.mapper.types.d.ts +6 -0
- package/lib/domain/types/checkoutSession.mapper.types.d.ts.map +1 -1
- package/lib/domain/types/paymentLink.mapper.types.d.ts +20 -0
- package/lib/domain/types/paymentLink.mapper.types.d.ts.map +1 -1
- package/lib/domain/types/plan.mapper.types.d.ts +10 -0
- package/lib/domain/types/plan.mapper.types.d.ts.map +1 -1
- package/lib/domain/types/stripe.dto.types.d.ts +6 -8
- package/lib/domain/types/stripe.dto.types.d.ts.map +1 -1
- package/lib/domain/types/stripe.entity.types.d.ts.map +1 -1
- package/lib/domain/types/subscription.mapper.types.d.ts +15 -0
- package/lib/domain/types/subscription.mapper.types.d.ts.map +1 -1
- package/lib/eject/domain/schemas/billingPortal.schema.ts +1 -1
- package/lib/eject/domain/schemas/checkoutSession.schema.ts +5 -3
- package/lib/eject/domain/schemas/paymentLink.schema.ts +12 -1
- package/lib/eject/domain/schemas/plan.schema.ts +1 -1
- package/lib/eject/domain/schemas/subscription.schema.ts +2 -1
- package/lib/eject/domain/types/billingPortal.mapper.types.ts +17 -0
- package/lib/eject/domain/types/checkoutSession.mapper.types.ts +9 -0
- package/lib/eject/domain/types/paymentLink.mapper.types.ts +36 -0
- package/lib/eject/domain/types/plan.mapper.types.ts +16 -0
- package/lib/eject/domain/types/stripe.dto.types.ts +6 -8
- package/lib/eject/domain/types/stripe.entity.types.ts +0 -6
- package/lib/eject/domain/types/subscription.mapper.types.ts +21 -0
- package/lib/eject/services/billingPortal.service.ts +24 -0
- package/lib/eject/services/checkoutSession.service.ts +54 -9
- package/lib/eject/services/paymentLink.service.ts +149 -25
- package/lib/eject/services/plan.service.ts +34 -4
- package/lib/eject/services/subscription.service.ts +42 -1
- package/lib/eject/services/webhook.service.ts +10 -1
- package/lib/services/billingPortal.service.d.ts.map +1 -1
- package/lib/services/checkoutSession.service.d.ts +8 -0
- package/lib/services/checkoutSession.service.d.ts.map +1 -1
- package/lib/services/index.js +238 -25
- package/lib/services/index.mjs +238 -25
- package/lib/services/paymentLink.service.d.ts +12 -0
- package/lib/services/paymentLink.service.d.ts.map +1 -1
- package/lib/services/plan.service.d.ts.map +1 -1
- package/lib/services/subscription.service.d.ts.map +1 -1
- package/lib/services/webhook.service.d.ts.map +1 -1
- package/package.json +8 -8
|
@@ -9,12 +9,26 @@ export type StripePaymentLinkMappers<
|
|
|
9
9
|
Dto extends StripePaymentLinkDtos<StatusEnum>
|
|
10
10
|
> = {
|
|
11
11
|
PaymentLinkMapper: {
|
|
12
|
+
/** Validate and restore declared DTO fields after a JSON cache read. */
|
|
13
|
+
normalizeCachedDto?: (
|
|
14
|
+
value: Dto['PaymentLinkMapper']
|
|
15
|
+
) => Dto['PaymentLinkMapper'];
|
|
12
16
|
entity: Entities['PaymentLinkMapper'];
|
|
13
17
|
toDto: (
|
|
14
18
|
entity: InferEntity<Entities['PaymentLinkMapper']>
|
|
15
19
|
) => Promise<Dto['PaymentLinkMapper']>;
|
|
20
|
+
validateCachedRead?: (existing: Dto['PaymentLinkMapper']) => void;
|
|
21
|
+
/** Opt-in cached reference lookup; does not prove provider account ownership. */
|
|
22
|
+
readCachedDto?: (
|
|
23
|
+
existing: Dto['PaymentLinkMapper'],
|
|
24
|
+
retrieve: (providerId: string) => Promise<Stripe.PaymentLink>
|
|
25
|
+
) => Promise<Dto['PaymentLinkMapper']>;
|
|
16
26
|
};
|
|
17
27
|
CreatePaymentLinkMapper: {
|
|
28
|
+
/** Provider effects already occurred; validate shape before local persistence. */
|
|
29
|
+
validateProviderCreate?: (provider: Stripe.PaymentLink) => Promise<void>;
|
|
30
|
+
/** Pure configured validation before any provider creation effects. */
|
|
31
|
+
validateCreate?: (dto: Dto['CreatePaymentLinkMapper']) => Promise<void>;
|
|
18
32
|
entity: Entities['CreatePaymentLinkMapper'];
|
|
19
33
|
toEntity: (
|
|
20
34
|
dto: Dto['CreatePaymentLinkMapper'],
|
|
@@ -24,6 +38,28 @@ export type StripePaymentLinkMappers<
|
|
|
24
38
|
) => Promise<InferEntity<Entities['CreatePaymentLinkMapper']>>;
|
|
25
39
|
};
|
|
26
40
|
UpdatePaymentLinkMapper: {
|
|
41
|
+
/** Validate the already-applied provider result and prepare one local update.
|
|
42
|
+
* `provider.id` is the stored Stripe id, which the service has already
|
|
43
|
+
* matched; `dto.id` stays the public id. Does not resolve aliases or
|
|
44
|
+
* establish provider account ownership.
|
|
45
|
+
*/
|
|
46
|
+
toLocalUpdate?: (
|
|
47
|
+
dto: Dto['UpdatePaymentLinkMapper'],
|
|
48
|
+
provider: Stripe.PaymentLink
|
|
49
|
+
) => Promise<Dto['UpdatePaymentLinkMapper']>;
|
|
50
|
+
toCachedDto?: (
|
|
51
|
+
dto: Dto['UpdatePaymentLinkMapper'],
|
|
52
|
+
existing: Dto['PaymentLinkMapper'],
|
|
53
|
+
em: EntityManager,
|
|
54
|
+
...args: unknown[]
|
|
55
|
+
) => Promise<Dto['PaymentLinkMapper']>;
|
|
56
|
+
/** Validate a patch against the normalized cached DTO before provider I/O.
|
|
57
|
+
* This hook does not resolve provider IDs or authorize provider mutations.
|
|
58
|
+
*/
|
|
59
|
+
validateCachedUpdate?: (
|
|
60
|
+
dto: Dto['UpdatePaymentLinkMapper'],
|
|
61
|
+
existing: Dto['PaymentLinkMapper']
|
|
62
|
+
) => Promise<void>;
|
|
27
63
|
entity: Entities['UpdatePaymentLinkMapper'];
|
|
28
64
|
toEntity: (
|
|
29
65
|
dto: Dto['UpdatePaymentLinkMapper'],
|
|
@@ -8,12 +8,21 @@ export type StripePlanMappers<
|
|
|
8
8
|
Dto extends StripePlanDtos
|
|
9
9
|
> = {
|
|
10
10
|
PlanMapper: {
|
|
11
|
+
/** Read-result identity check; does not establish provider account ownership. */
|
|
12
|
+
validateProviderRead?: (
|
|
13
|
+
expectedExternalId: string,
|
|
14
|
+
provider: Stripe.Plan
|
|
15
|
+
) => Promise<void>;
|
|
11
16
|
entity: Entities['PlanMapper'];
|
|
12
17
|
toDto: (
|
|
13
18
|
entity: InferEntity<Entities['PlanMapper']>
|
|
14
19
|
) => Promise<Dto['PlanMapper']>;
|
|
15
20
|
};
|
|
16
21
|
CreatePlanMapper: {
|
|
22
|
+
/** Provider effects already occurred; validate shape before local persistence. */
|
|
23
|
+
validateProviderCreate?: (provider: Stripe.Plan) => Promise<void>;
|
|
24
|
+
/** Pure configured validation before any provider creation effects. */
|
|
25
|
+
validateCreate?: (dto: Dto['CreatePlanMapper']) => Promise<void>;
|
|
17
26
|
entity: Entities['CreatePlanMapper'];
|
|
18
27
|
toEntity: (
|
|
19
28
|
dto: Dto['CreatePlanMapper'],
|
|
@@ -23,6 +32,13 @@ export type StripePlanMappers<
|
|
|
23
32
|
) => Promise<InferEntity<Entities['CreatePlanMapper']>>;
|
|
24
33
|
};
|
|
25
34
|
UpdatePlanMapper: {
|
|
35
|
+
/** Validate consumed provider references before a subsequent mutation. */
|
|
36
|
+
validateProviderUpdate?: (
|
|
37
|
+
dto: Dto['UpdatePlanMapper'],
|
|
38
|
+
provider: Stripe.Plan
|
|
39
|
+
) => Promise<void>;
|
|
40
|
+
/** Pure configured validation before any provider replacement effects. */
|
|
41
|
+
validateUpdate?: (dto: Dto['UpdatePlanMapper']) => Promise<void>;
|
|
26
42
|
entity: Entities['UpdatePlanMapper'];
|
|
27
43
|
toEntity: (
|
|
28
44
|
dto: Dto['UpdatePlanMapper'],
|
|
@@ -84,7 +84,7 @@ export type StripeCreateBillingPortalDto = Omit<
|
|
|
84
84
|
CreateBillingPortalDto,
|
|
85
85
|
'providerFields'
|
|
86
86
|
> & {
|
|
87
|
-
stripeFields
|
|
87
|
+
stripeFields?: Omit<
|
|
88
88
|
StripeBillingPortalSessionCreateParams,
|
|
89
89
|
BillingPortalOmissions
|
|
90
90
|
>;
|
|
@@ -126,10 +126,9 @@ export type StripeCreateCheckoutSessionDto<StatusEnum> = Omit<
|
|
|
126
126
|
typeof CurrencyEnum,
|
|
127
127
|
StatusEnum
|
|
128
128
|
>,
|
|
129
|
-
'providerFields'
|
|
129
|
+
'providerFields'
|
|
130
130
|
> & {
|
|
131
|
-
|
|
132
|
-
stripeFields: Omit<
|
|
131
|
+
stripeFields?: Omit<
|
|
133
132
|
StripeCheckoutSessionCreateParams,
|
|
134
133
|
CheckoutSessionOmissions
|
|
135
134
|
>;
|
|
@@ -151,9 +150,8 @@ export type StripeUpdateCheckoutSessionDto<StatusEnum> = Omit<
|
|
|
151
150
|
|
|
152
151
|
export type StripeCheckoutSessionDto<StatusEnum> = Omit<
|
|
153
152
|
CheckoutSessionDto<typeof PaymentMethodEnum, typeof CurrencyEnum, StatusEnum>,
|
|
154
|
-
'providerFields'
|
|
153
|
+
'providerFields'
|
|
155
154
|
> & {
|
|
156
|
-
uri: string;
|
|
157
155
|
stripeFields: StripeCheckoutSession;
|
|
158
156
|
};
|
|
159
157
|
|
|
@@ -210,7 +208,7 @@ export type StripeCreatePlanDto = Omit<
|
|
|
210
208
|
>,
|
|
211
209
|
'providerFields'
|
|
212
210
|
> & {
|
|
213
|
-
stripeFields
|
|
211
|
+
stripeFields?: Omit<StripePlanCreateParams, PlanOmissions>;
|
|
214
212
|
};
|
|
215
213
|
|
|
216
214
|
export type StripeUpdatePlanDto = Omit<
|
|
@@ -248,7 +246,7 @@ export type StripeCreateSubscriptionDto<PartyType> = Omit<
|
|
|
248
246
|
CreateSubscriptionDto<PartyType, typeof BillingProviderEnum>,
|
|
249
247
|
'providerFields'
|
|
250
248
|
> & {
|
|
251
|
-
stripeFields
|
|
249
|
+
stripeFields?: Omit<StripeSubscriptionCreateParams, SubscriptionOmissions>;
|
|
252
250
|
};
|
|
253
251
|
|
|
254
252
|
export type StripeUpdateSubscriptionDto<PartyType> = Omit<
|
|
@@ -6,12 +6,6 @@ import {
|
|
|
6
6
|
Plan,
|
|
7
7
|
Subscription
|
|
8
8
|
} from '../../persistence/entities';
|
|
9
|
-
import {
|
|
10
|
-
BillingProviderEnum,
|
|
11
|
-
CurrencyEnum,
|
|
12
|
-
PaymentMethodEnum,
|
|
13
|
-
PlanCadenceEnum
|
|
14
|
-
} from '../enum';
|
|
15
9
|
|
|
16
10
|
// Billing Portal Types
|
|
17
11
|
export type StripeBillingPortalEntities = {
|
|
@@ -9,12 +9,23 @@ export type StripeSubscriptionMappers<
|
|
|
9
9
|
Dto extends StripeSubscriptionDtos<PartyTypeEnum>
|
|
10
10
|
> = {
|
|
11
11
|
SubscriptionMapper: {
|
|
12
|
+
/** Read-result identity check; does not establish provider account ownership. */
|
|
13
|
+
validateProviderRead?: (
|
|
14
|
+
expectedExternalId: string,
|
|
15
|
+
provider: Stripe.Subscription
|
|
16
|
+
) => Promise<void>;
|
|
17
|
+
/** Trusted persisted party discriminators for local subscription lookups. */
|
|
18
|
+
partyTypes?: Readonly<{ user: string; organization: string }>;
|
|
12
19
|
entity: Entities['SubscriptionMapper'];
|
|
13
20
|
toDto: (
|
|
14
21
|
entity: InferEntity<Entities['SubscriptionMapper']>
|
|
15
22
|
) => Promise<Dto['SubscriptionMapper']>;
|
|
16
23
|
};
|
|
17
24
|
CreateSubscriptionMapper: {
|
|
25
|
+
/** Provider effects already occurred; validate shape before local persistence. */
|
|
26
|
+
validateProviderCreate?: (provider: Stripe.Subscription) => Promise<void>;
|
|
27
|
+
/** Pure configured validation before any provider creation effects. */
|
|
28
|
+
validateCreate?: (dto: Dto['CreateSubscriptionMapper']) => Promise<void>;
|
|
18
29
|
entity: Entities['CreateSubscriptionMapper'];
|
|
19
30
|
toEntity: (
|
|
20
31
|
dto: Dto['CreateSubscriptionMapper'],
|
|
@@ -24,6 +35,16 @@ export type StripeSubscriptionMappers<
|
|
|
24
35
|
) => Promise<InferEntity<Entities['CreateSubscriptionMapper']>>;
|
|
25
36
|
};
|
|
26
37
|
UpdateSubscriptionMapper: {
|
|
38
|
+
/** Validate consumed provider references before a subsequent mutation. */
|
|
39
|
+
validateProviderUpdate?: (
|
|
40
|
+
dto: Dto['UpdateSubscriptionMapper'],
|
|
41
|
+
provider: Stripe.Subscription
|
|
42
|
+
) => Promise<void>;
|
|
43
|
+
/** Configured preflight; changing customer ownership needs a separate workflow. */
|
|
44
|
+
validateUpdate?: (
|
|
45
|
+
dto: Dto['UpdateSubscriptionMapper'],
|
|
46
|
+
existing: Dto['SubscriptionMapper']
|
|
47
|
+
) => Promise<void>;
|
|
27
48
|
entity: Entities['UpdateSubscriptionMapper'];
|
|
28
49
|
toEntity: (
|
|
29
50
|
dto: Dto['UpdateSubscriptionMapper'],
|
|
@@ -78,11 +78,15 @@ export class StripeBillingPortalService<
|
|
|
78
78
|
billingPortalDto: StripeCreateBillingPortalDto,
|
|
79
79
|
...args: unknown[]
|
|
80
80
|
): Promise<Dto['BillingPortalMapper']> {
|
|
81
|
+
await this.mappers.CreateBillingPortalMapper.validateCreate?.(billingPortalDto);
|
|
82
|
+
|
|
81
83
|
const session = await this.stripeClient.billingPortal.sessions.create({
|
|
82
84
|
...billingPortalDto.stripeFields,
|
|
83
85
|
customer: billingPortalDto.customerId
|
|
84
86
|
});
|
|
85
87
|
|
|
88
|
+
await this.mappers.CreateBillingPortalMapper.validateProviderCreate?.(session);
|
|
89
|
+
|
|
86
90
|
return await this.baseBillingPortalService.createBillingPortalSession(
|
|
87
91
|
{
|
|
88
92
|
...billingPortalDto,
|
|
@@ -116,11 +120,31 @@ export class StripeBillingPortalService<
|
|
|
116
120
|
await this.baseBillingPortalService.getBillingPortalSession({
|
|
117
121
|
id: billingPortalDto.id
|
|
118
122
|
});
|
|
123
|
+
if (
|
|
124
|
+
!this.options?.enableDatabaseBackup &&
|
|
125
|
+
this.mappers.UpdateBillingPortalMapper.toCachedDto &&
|
|
126
|
+
billingPortalDto.customerId !== undefined &&
|
|
127
|
+
billingPortalDto.customerId !== existingSession.customerId
|
|
128
|
+
) {
|
|
129
|
+
throw new Error('Cached session identity cannot change');
|
|
130
|
+
}
|
|
131
|
+
if (
|
|
132
|
+
!this.options?.enableDatabaseBackup &&
|
|
133
|
+
this.mappers.UpdateBillingPortalMapper.toCachedDto &&
|
|
134
|
+
this.mappers.UpdateBillingPortalMapper.validateCachedUpdate
|
|
135
|
+
) {
|
|
136
|
+
await this.mappers.UpdateBillingPortalMapper.validateCachedUpdate(
|
|
137
|
+
billingPortalDto,
|
|
138
|
+
existingSession
|
|
139
|
+
);
|
|
140
|
+
}
|
|
119
141
|
const session = await this.stripeClient.billingPortal.sessions.create({
|
|
120
142
|
...billingPortalDto.stripeFields,
|
|
121
143
|
customer: billingPortalDto.customerId || existingSession.customerId
|
|
122
144
|
});
|
|
123
145
|
|
|
146
|
+
await this.mappers.CreateBillingPortalMapper.validateProviderCreate?.(session);
|
|
147
|
+
|
|
124
148
|
return await this.baseBillingPortalService.updateBillingPortalSession(
|
|
125
149
|
{
|
|
126
150
|
...billingPortalDto,
|
|
@@ -93,6 +93,8 @@ export class StripeCheckoutSessionService<
|
|
|
93
93
|
checkoutSessionDto: StripeCreateCheckoutSessionDto<StatusEnum>,
|
|
94
94
|
...args: unknown[]
|
|
95
95
|
): Promise<Dto['CheckoutSessionMapper']> {
|
|
96
|
+
await this.mappers.CreateCheckoutSessionMapper.validateCreate?.(checkoutSessionDto);
|
|
97
|
+
|
|
96
98
|
const session = await this.stripeClient.checkout.sessions.create({
|
|
97
99
|
...checkoutSessionDto.stripeFields,
|
|
98
100
|
mode: 'subscription',
|
|
@@ -102,6 +104,8 @@ export class StripeCheckoutSessionService<
|
|
|
102
104
|
cancel_url: checkoutSessionDto.cancelRedirectUri
|
|
103
105
|
});
|
|
104
106
|
|
|
107
|
+
await this.mappers.CreateCheckoutSessionMapper.validateProviderCreate?.(session);
|
|
108
|
+
|
|
105
109
|
return await this.baseCheckoutSessionService.createCheckoutSession(
|
|
106
110
|
{
|
|
107
111
|
...checkoutSessionDto,
|
|
@@ -119,6 +123,17 @@ export class StripeCheckoutSessionService<
|
|
|
119
123
|
async getCheckoutSession(
|
|
120
124
|
idDto: IdDto
|
|
121
125
|
): Promise<Dto['CheckoutSessionMapper']> {
|
|
126
|
+
const readCachedDto = this.mappers.CheckoutSessionMapper.readCachedDto;
|
|
127
|
+
if (readCachedDto) {
|
|
128
|
+
const existing =
|
|
129
|
+
await this.baseCheckoutSessionService.getCheckoutSession(idDto);
|
|
130
|
+
if (existing.id !== idDto.id) {
|
|
131
|
+
throw new Error('Cached checkout identity does not match');
|
|
132
|
+
}
|
|
133
|
+
return readCachedDto(existing, (providerId) =>
|
|
134
|
+
this.stripeClient.checkout.sessions.retrieve(providerId)
|
|
135
|
+
);
|
|
136
|
+
}
|
|
122
137
|
const session = await this.stripeClient.checkout.sessions.retrieve(
|
|
123
138
|
idDto.id
|
|
124
139
|
);
|
|
@@ -130,26 +145,56 @@ export class StripeCheckoutSessionService<
|
|
|
130
145
|
return databaseCheckoutSession;
|
|
131
146
|
}
|
|
132
147
|
|
|
148
|
+
/**
|
|
149
|
+
* The Stripe id (`cs_...`) stored with a checkout session when it was
|
|
150
|
+
* created. Public ids are local; Stripe only knows its own id. The stored
|
|
151
|
+
* provider object's id wins; a record without one is still usable when its
|
|
152
|
+
* own id is a Stripe id. Anything else fails here rather than sending Stripe
|
|
153
|
+
* a local id.
|
|
154
|
+
*/
|
|
155
|
+
protected async stripeCheckoutSessionId(id: string): Promise<string> {
|
|
156
|
+
const checkoutSession =
|
|
157
|
+
await this.baseCheckoutSessionService.getCheckoutSession({ id });
|
|
158
|
+
const stored = (
|
|
159
|
+
checkoutSession?.stripeFields as { id?: unknown } | undefined
|
|
160
|
+
)?.id;
|
|
161
|
+
const stripeId = stored === undefined ? checkoutSession?.id : stored;
|
|
162
|
+
if (typeof stripeId !== 'string' || !/^cs_[A-Za-z0-9_]+$/.test(stripeId)) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Checkout session ${id} has no stored Stripe checkout session id`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return stripeId;
|
|
168
|
+
}
|
|
169
|
+
|
|
133
170
|
async expireCheckoutSession({ id }: IdDto): Promise<void> {
|
|
134
|
-
await this.stripeClient.checkout.sessions.expire(
|
|
171
|
+
await this.stripeClient.checkout.sessions.expire(
|
|
172
|
+
await this.stripeCheckoutSessionId(id)
|
|
173
|
+
);
|
|
135
174
|
await this.baseCheckoutSessionService.expireCheckoutSession({ id });
|
|
136
175
|
}
|
|
137
176
|
|
|
138
177
|
async handleCheckoutSuccess({ id }: IdDto): Promise<void> {
|
|
139
|
-
await this.stripeClient.checkout.sessions.update(
|
|
140
|
-
|
|
141
|
-
|
|
178
|
+
await this.stripeClient.checkout.sessions.update(
|
|
179
|
+
await this.stripeCheckoutSessionId(id),
|
|
180
|
+
{
|
|
181
|
+
metadata: {
|
|
182
|
+
status: 'SUCCESS'
|
|
183
|
+
}
|
|
142
184
|
}
|
|
143
|
-
|
|
185
|
+
);
|
|
144
186
|
await this.baseCheckoutSessionService.handleCheckoutSuccess({ id });
|
|
145
187
|
}
|
|
146
188
|
|
|
147
189
|
async handleCheckoutFailure({ id }: IdDto): Promise<void> {
|
|
148
|
-
await this.stripeClient.checkout.sessions.update(
|
|
149
|
-
|
|
150
|
-
|
|
190
|
+
await this.stripeClient.checkout.sessions.update(
|
|
191
|
+
await this.stripeCheckoutSessionId(id),
|
|
192
|
+
{
|
|
193
|
+
metadata: {
|
|
194
|
+
status: 'FAILED'
|
|
195
|
+
}
|
|
151
196
|
}
|
|
152
|
-
|
|
197
|
+
);
|
|
153
198
|
await this.baseCheckoutSessionService.handleCheckoutFailure({ id });
|
|
154
199
|
}
|
|
155
200
|
}
|
|
@@ -21,24 +21,44 @@ import {
|
|
|
21
21
|
} from '../domain/types/stripe.dto.types';
|
|
22
22
|
import { StripePaymentLinkEntities } from '../domain/types/stripe.entity.types';
|
|
23
23
|
|
|
24
|
+
/** Match the required provider object/line-items structure without modeling the SDK. */
|
|
25
|
+
function validatePaymentLinkCreateFields(value: unknown): void {
|
|
26
|
+
if (
|
|
27
|
+
!value ||
|
|
28
|
+
typeof value !== 'object' ||
|
|
29
|
+
Array.isArray(value) ||
|
|
30
|
+
!Array.isArray((value as Record<string, unknown>).line_items) ||
|
|
31
|
+
!(value as { line_items: unknown[] }).line_items.every(
|
|
32
|
+
(item) =>
|
|
33
|
+
item !== null && typeof item === 'object' && !Array.isArray(item)
|
|
34
|
+
)
|
|
35
|
+
) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
'Payment link provider options require a line_items array of objects'
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
24
42
|
export class StripePaymentLinkService<
|
|
25
43
|
SchemaValidator extends AnySchemaValidator,
|
|
26
44
|
StatusEnum,
|
|
27
45
|
Entities extends StripePaymentLinkEntities,
|
|
28
|
-
Dto extends
|
|
29
|
-
StripePaymentLinkDtos<StatusEnum>
|
|
30
|
-
> implements
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
Dto extends
|
|
47
|
+
StripePaymentLinkDtos<StatusEnum> = StripePaymentLinkDtos<StatusEnum>
|
|
48
|
+
> implements
|
|
49
|
+
PaymentLinkService<
|
|
50
|
+
PaymentMethodEnum,
|
|
51
|
+
typeof CurrencyEnum,
|
|
52
|
+
StatusEnum,
|
|
53
|
+
{
|
|
54
|
+
CreatePaymentLinkDto: StripeCreatePaymentLinkDto<StatusEnum>;
|
|
55
|
+
UpdatePaymentLinkDto: StripeUpdatePaymentLinkDto<StatusEnum>;
|
|
56
|
+
PaymentLinkDto: StripePaymentLinkDto<StatusEnum>;
|
|
57
|
+
IdDto: IdDto;
|
|
58
|
+
IdsDto: IdsDto;
|
|
59
|
+
}
|
|
60
|
+
>
|
|
61
|
+
{
|
|
42
62
|
basePaymentLinkService: BasePaymentLinkService<
|
|
43
63
|
SchemaValidator,
|
|
44
64
|
PaymentMethodEnum,
|
|
@@ -47,6 +67,7 @@ export class StripePaymentLinkService<
|
|
|
47
67
|
Entities,
|
|
48
68
|
Dto
|
|
49
69
|
>;
|
|
70
|
+
private readonly maxConfiguredListRecords: number;
|
|
50
71
|
protected readonly stripeClient: Stripe;
|
|
51
72
|
protected readonly em: EntityManager;
|
|
52
73
|
protected readonly cache: TtlCache;
|
|
@@ -67,9 +88,19 @@ export class StripePaymentLinkService<
|
|
|
67
88
|
mappers: StripePaymentLinkMappers<StatusEnum, Entities, Dto>,
|
|
68
89
|
readonly options?: {
|
|
69
90
|
enableDatabaseBackup?: boolean;
|
|
91
|
+
/** Trusted configured-cache list limit; defaults to 100, maximum 1000. */
|
|
92
|
+
maxConfiguredListRecords?: number;
|
|
70
93
|
telemetry?: TelemetryOptions;
|
|
71
94
|
}
|
|
72
95
|
) {
|
|
96
|
+
const limit =
|
|
97
|
+
options?.maxConfiguredListRecords === undefined
|
|
98
|
+
? 100
|
|
99
|
+
: options.maxConfiguredListRecords;
|
|
100
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000) {
|
|
101
|
+
throw new Error('Invalid configured payment link list limit');
|
|
102
|
+
}
|
|
103
|
+
this.maxConfiguredListRecords = limit;
|
|
73
104
|
this.stripeClient = stripeClient;
|
|
74
105
|
this.em = em;
|
|
75
106
|
this.cache = cache;
|
|
@@ -96,12 +127,19 @@ export class StripePaymentLinkService<
|
|
|
96
127
|
paymentLinkDto: StripeCreatePaymentLinkDto<StatusEnum>,
|
|
97
128
|
...args: unknown[]
|
|
98
129
|
): Promise<Dto['PaymentLinkMapper']> {
|
|
130
|
+
await this.mappers.CreatePaymentLinkMapper.validateCreate?.(paymentLinkDto);
|
|
131
|
+
|
|
132
|
+
validatePaymentLinkCreateFields(paymentLinkDto.stripeFields);
|
|
99
133
|
const session = await this.stripeClient.paymentLinks.create({
|
|
100
134
|
...paymentLinkDto.stripeFields,
|
|
101
135
|
payment_method_types: paymentLinkDto.paymentMethods,
|
|
102
136
|
currency: paymentLinkDto.currency as string
|
|
103
137
|
});
|
|
104
138
|
|
|
139
|
+
await this.mappers.CreatePaymentLinkMapper.validateProviderCreate?.(
|
|
140
|
+
session
|
|
141
|
+
);
|
|
142
|
+
|
|
105
143
|
const paymentLink = await this.basePaymentLinkService.createPaymentLink(
|
|
106
144
|
{
|
|
107
145
|
...paymentLinkDto,
|
|
@@ -123,24 +161,68 @@ export class StripePaymentLinkService<
|
|
|
123
161
|
return paymentLink;
|
|
124
162
|
}
|
|
125
163
|
|
|
164
|
+
/**
|
|
165
|
+
* The Stripe id (`plink_...`) stored with a payment link when it was created.
|
|
166
|
+
* Public ids are local; Stripe only knows its own id. The stored provider
|
|
167
|
+
* object's id wins; a record without one is still usable when its own id is a
|
|
168
|
+
* Stripe id. Anything else fails here rather than sending Stripe a local id.
|
|
169
|
+
*/
|
|
170
|
+
protected stripePaymentLinkId(
|
|
171
|
+
paymentLink: Dto['PaymentLinkMapper'],
|
|
172
|
+
id: string
|
|
173
|
+
): string {
|
|
174
|
+
const stored = (paymentLink?.stripeFields as { id?: unknown } | undefined)
|
|
175
|
+
?.id;
|
|
176
|
+
const stripeId = stored === undefined ? paymentLink?.id : stored;
|
|
177
|
+
if (
|
|
178
|
+
typeof stripeId !== 'string' ||
|
|
179
|
+
!/^plink_[A-Za-z0-9_]+$/.test(stripeId)
|
|
180
|
+
) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Payment link ${id} has no stored Stripe payment link id`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return stripeId;
|
|
186
|
+
}
|
|
187
|
+
|
|
126
188
|
async updatePaymentLink(
|
|
127
189
|
paymentLinkDto: StripeUpdatePaymentLinkDto<StatusEnum>,
|
|
128
190
|
...args: unknown[]
|
|
129
191
|
): Promise<Dto['PaymentLinkMapper']> {
|
|
130
|
-
const
|
|
131
|
-
paymentLinkDto.id
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
192
|
+
const existing = await this.basePaymentLinkService.getPaymentLink({
|
|
193
|
+
id: paymentLinkDto.id
|
|
194
|
+
});
|
|
195
|
+
if (this.mappers.UpdatePaymentLinkMapper.validateCachedUpdate) {
|
|
196
|
+
await this.mappers.UpdatePaymentLinkMapper.validateCachedUpdate(
|
|
197
|
+
paymentLinkDto,
|
|
198
|
+
existing
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
const stripeId = this.stripePaymentLinkId(existing, paymentLinkDto.id);
|
|
202
|
+
const session = await this.stripeClient.paymentLinks.update(stripeId, {
|
|
203
|
+
...paymentLinkDto.stripeFields,
|
|
204
|
+
payment_method_types: paymentLinkDto.paymentMethods
|
|
205
|
+
});
|
|
206
|
+
if (session.id !== stripeId) {
|
|
207
|
+
throw new Error('Invalid Stripe billing response');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const toLocalUpdate = this.mappers.UpdatePaymentLinkMapper.toLocalUpdate;
|
|
211
|
+
if (toLocalUpdate) {
|
|
212
|
+
const patch = await toLocalUpdate(paymentLinkDto, session);
|
|
213
|
+
return this.basePaymentLinkService.updatePaymentLink(
|
|
214
|
+
patch,
|
|
215
|
+
this.em,
|
|
216
|
+
session,
|
|
217
|
+
...args
|
|
218
|
+
);
|
|
219
|
+
}
|
|
137
220
|
|
|
138
221
|
const paymentLink = await this.basePaymentLinkService.updatePaymentLink(
|
|
139
222
|
await this.mappers.PaymentLinkMapper.toDto(
|
|
140
223
|
await this.mappers.UpdatePaymentLinkMapper.toEntity(
|
|
141
224
|
{
|
|
142
225
|
...paymentLinkDto,
|
|
143
|
-
id: session.id,
|
|
144
226
|
amount:
|
|
145
227
|
session.line_items?.data.reduce<number>(
|
|
146
228
|
(total, item) => total + item.amount_total,
|
|
@@ -160,6 +242,16 @@ export class StripePaymentLinkService<
|
|
|
160
242
|
}
|
|
161
243
|
|
|
162
244
|
async getPaymentLink({ id }: IdDto): Promise<Dto['PaymentLinkMapper']> {
|
|
245
|
+
const readCachedDto = this.mappers.PaymentLinkMapper.readCachedDto;
|
|
246
|
+
if (readCachedDto) {
|
|
247
|
+
const existing = await this.basePaymentLinkService.getPaymentLink({ id });
|
|
248
|
+
if (existing.id !== id) {
|
|
249
|
+
throw new Error('Cached payment link identity does not match');
|
|
250
|
+
}
|
|
251
|
+
return readCachedDto(existing, (providerId) =>
|
|
252
|
+
this.stripeClient.paymentLinks.retrieve(providerId)
|
|
253
|
+
);
|
|
254
|
+
}
|
|
163
255
|
const stripePaymentLink = await this.stripeClient.paymentLinks.retrieve(id);
|
|
164
256
|
|
|
165
257
|
const databasePaymentLink =
|
|
@@ -171,7 +263,11 @@ export class StripePaymentLinkService<
|
|
|
171
263
|
}
|
|
172
264
|
|
|
173
265
|
async expirePaymentLink({ id }: IdDto): Promise<void> {
|
|
174
|
-
|
|
266
|
+
const stripeId = this.stripePaymentLinkId(
|
|
267
|
+
await this.basePaymentLinkService.getPaymentLink({ id }),
|
|
268
|
+
id
|
|
269
|
+
);
|
|
270
|
+
await this.stripeClient.paymentLinks.update(stripeId, {
|
|
175
271
|
metadata: {
|
|
176
272
|
status: 'EXPIRED'
|
|
177
273
|
}
|
|
@@ -180,7 +276,11 @@ export class StripePaymentLinkService<
|
|
|
180
276
|
}
|
|
181
277
|
|
|
182
278
|
async handlePaymentSuccess({ id }: IdDto): Promise<void> {
|
|
183
|
-
|
|
279
|
+
const stripeId = this.stripePaymentLinkId(
|
|
280
|
+
await this.basePaymentLinkService.getPaymentLink({ id }),
|
|
281
|
+
id
|
|
282
|
+
);
|
|
283
|
+
await this.stripeClient.paymentLinks.update(stripeId, {
|
|
184
284
|
metadata: {
|
|
185
285
|
status: 'COMPLETED'
|
|
186
286
|
}
|
|
@@ -189,7 +289,11 @@ export class StripePaymentLinkService<
|
|
|
189
289
|
}
|
|
190
290
|
|
|
191
291
|
async handlePaymentFailure({ id }: IdDto): Promise<void> {
|
|
192
|
-
|
|
292
|
+
const stripeId = this.stripePaymentLinkId(
|
|
293
|
+
await this.basePaymentLinkService.getPaymentLink({ id }),
|
|
294
|
+
id
|
|
295
|
+
);
|
|
296
|
+
await this.stripeClient.paymentLinks.update(stripeId, {
|
|
193
297
|
metadata: {
|
|
194
298
|
status: 'FAILED'
|
|
195
299
|
}
|
|
@@ -199,6 +303,26 @@ export class StripePaymentLinkService<
|
|
|
199
303
|
|
|
200
304
|
async listPaymentLinks(idsDto?: IdsDto): Promise<Dto['PaymentLinkMapper'][]> {
|
|
201
305
|
this.openTelemetryCollector.log('info', idsDto ?? 'idsDto is undefined');
|
|
306
|
+
const { readCachedDto, validateCachedRead } =
|
|
307
|
+
this.mappers.PaymentLinkMapper;
|
|
308
|
+
if (readCachedDto && validateCachedRead) {
|
|
309
|
+
const cached = await this.basePaymentLinkService.listPaymentLinks(
|
|
310
|
+
idsDto,
|
|
311
|
+
this.maxConfiguredListRecords
|
|
312
|
+
);
|
|
313
|
+
for (const value of cached) validateCachedRead(value);
|
|
314
|
+
const active: Dto['PaymentLinkMapper'][] = [];
|
|
315
|
+
for (const value of cached) {
|
|
316
|
+
const fresh = await readCachedDto(value, (providerId) =>
|
|
317
|
+
this.stripeClient.paymentLinks.retrieve(providerId)
|
|
318
|
+
);
|
|
319
|
+
if (typeof fresh.stripeFields.active !== 'boolean') {
|
|
320
|
+
throw new Error('Invalid payment link active state');
|
|
321
|
+
}
|
|
322
|
+
if (fresh.stripeFields.active) active.push(fresh);
|
|
323
|
+
}
|
|
324
|
+
return active;
|
|
325
|
+
}
|
|
202
326
|
const stripePaymentLinks = await this.stripeClient.paymentLinks.list({
|
|
203
327
|
active: true
|
|
204
328
|
});
|