@forklaunch/implementation-billing-stripe 0.0.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.
- package/LICENSE +21 -0
- package/lib/domain/index.d.mts +189 -0
- package/lib/domain/index.d.ts +189 -0
- package/lib/domain/index.js +231 -0
- package/lib/domain/index.mjs +201 -0
- package/lib/eject/domain/schemas/billingPortal.schema.ts +37 -0
- package/lib/eject/domain/schemas/checkoutSession.schema.ts +74 -0
- package/lib/eject/domain/schemas/index.ts +5 -0
- package/lib/eject/domain/schemas/paymentLink.schema.ts +62 -0
- package/lib/eject/domain/schemas/plan.schema.ts +68 -0
- package/lib/eject/domain/schemas/subscription.schema.ts +78 -0
- package/lib/eject/services/billingPortal.service.ts +185 -0
- package/lib/eject/services/checkoutSession.service.ts +179 -0
- package/lib/eject/services/index.ts +6 -0
- package/lib/eject/services/paymentLink.service.ts +239 -0
- package/lib/eject/services/plan.service.ts +219 -0
- package/lib/eject/services/subscription.service.ts +285 -0
- package/lib/eject/services/webhook.service.ts +272 -0
- package/lib/eject/types/index.ts +2 -0
- package/lib/eject/types/stripe.dto.types.ts +214 -0
- package/lib/eject/types/stripe.entity.types.ts +83 -0
- package/lib/schemas/index.d.mts +376 -0
- package/lib/schemas/index.d.ts +376 -0
- package/lib/schemas/index.js +704 -0
- package/lib/schemas/index.mjs +737 -0
- package/lib/services/index.d.mts +223 -0
- package/lib/services/index.d.ts +223 -0
- package/lib/services/index.js +788 -0
- package/lib/services/index.mjs +776 -0
- package/lib/types/index.d.mts +121 -0
- package/lib/types/index.d.ts +121 -0
- package/lib/types/index.js +18 -0
- package/lib/types/index.mjs +0 -0
- package/package.json +74 -0
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
// services/billingPortal.service.ts
|
|
2
|
+
import { BaseBillingPortalService } from "@forklaunch/implementation-billing-base/services";
|
|
3
|
+
import {
|
|
4
|
+
IdentityRequestMapper,
|
|
5
|
+
IdentityResponseMapper,
|
|
6
|
+
transformIntoInternalMapper
|
|
7
|
+
} from "@forklaunch/internal";
|
|
8
|
+
var StripeBillingPortalService = class {
|
|
9
|
+
constructor(stripeClient, em, cache, openTelemetryCollector, schemaValidator, mappers, options) {
|
|
10
|
+
this.stripeClient = stripeClient;
|
|
11
|
+
this.em = em;
|
|
12
|
+
this.cache = cache;
|
|
13
|
+
this.openTelemetryCollector = openTelemetryCollector;
|
|
14
|
+
this.schemaValidator = schemaValidator;
|
|
15
|
+
this.mappers = mappers;
|
|
16
|
+
this.options = options;
|
|
17
|
+
this._mappers = transformIntoInternalMapper(mappers, schemaValidator);
|
|
18
|
+
this.baseBillingPortalService = new BaseBillingPortalService(
|
|
19
|
+
em,
|
|
20
|
+
cache,
|
|
21
|
+
openTelemetryCollector,
|
|
22
|
+
schemaValidator,
|
|
23
|
+
{
|
|
24
|
+
BillingPortalMapper: IdentityResponseMapper,
|
|
25
|
+
CreateBillingPortalMapper: IdentityRequestMapper,
|
|
26
|
+
UpdateBillingPortalMapper: IdentityRequestMapper
|
|
27
|
+
},
|
|
28
|
+
options
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
billingPortalSessionExpiryDurationMs = 5 * 60 * 1e3;
|
|
32
|
+
baseBillingPortalService;
|
|
33
|
+
_mappers;
|
|
34
|
+
async createBillingPortalSession(billingPortalDto) {
|
|
35
|
+
const session = await this.stripeClient.billingPortal.sessions.create({
|
|
36
|
+
...billingPortalDto.stripeFields,
|
|
37
|
+
customer: billingPortalDto.customerId
|
|
38
|
+
});
|
|
39
|
+
const billingPortalEntity = await this.baseBillingPortalService.createBillingPortalSession(
|
|
40
|
+
await this._mappers.CreateBillingPortalMapper.deserializeDtoToEntity(
|
|
41
|
+
{
|
|
42
|
+
...billingPortalDto,
|
|
43
|
+
id: session.id,
|
|
44
|
+
uri: session.url,
|
|
45
|
+
expiresAt: new Date(
|
|
46
|
+
Date.now() + this.billingPortalSessionExpiryDurationMs
|
|
47
|
+
)
|
|
48
|
+
},
|
|
49
|
+
this.em,
|
|
50
|
+
session
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
return this._mappers.BillingPortalMapper.serializeEntityToDto(
|
|
54
|
+
billingPortalEntity
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
async getBillingPortalSession(idDto) {
|
|
58
|
+
const billingPortalEntity = await this.baseBillingPortalService.getBillingPortalSession(idDto);
|
|
59
|
+
return this._mappers.BillingPortalMapper.serializeEntityToDto(
|
|
60
|
+
billingPortalEntity
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
async expireBillingPortalSession(idDto) {
|
|
64
|
+
return this.baseBillingPortalService.expireBillingPortalSession(idDto);
|
|
65
|
+
}
|
|
66
|
+
async updateBillingPortalSession(billingPortalDto) {
|
|
67
|
+
const existingSession = await this.baseBillingPortalService.getBillingPortalSession({
|
|
68
|
+
id: billingPortalDto.id
|
|
69
|
+
});
|
|
70
|
+
const session = await this.stripeClient.billingPortal.sessions.create({
|
|
71
|
+
...billingPortalDto.stripeFields,
|
|
72
|
+
customer: existingSession.customerId
|
|
73
|
+
});
|
|
74
|
+
const baseBillingPortalDto = await this.baseBillingPortalService.updateBillingPortalSession(
|
|
75
|
+
await this._mappers.UpdateBillingPortalMapper.deserializeDtoToEntity(
|
|
76
|
+
{
|
|
77
|
+
...billingPortalDto,
|
|
78
|
+
id: session.id,
|
|
79
|
+
uri: session.url,
|
|
80
|
+
expiresAt: new Date(
|
|
81
|
+
Date.now() + this.billingPortalSessionExpiryDurationMs
|
|
82
|
+
)
|
|
83
|
+
},
|
|
84
|
+
this.em,
|
|
85
|
+
session
|
|
86
|
+
)
|
|
87
|
+
);
|
|
88
|
+
return this._mappers.BillingPortalMapper.serializeEntityToDto(
|
|
89
|
+
baseBillingPortalDto
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// services/checkoutSession.service.ts
|
|
95
|
+
import { BaseCheckoutSessionService } from "@forklaunch/implementation-billing-base/services";
|
|
96
|
+
import {
|
|
97
|
+
IdentityRequestMapper as IdentityRequestMapper2,
|
|
98
|
+
IdentityResponseMapper as IdentityResponseMapper2,
|
|
99
|
+
transformIntoInternalMapper as transformIntoInternalMapper2
|
|
100
|
+
} from "@forklaunch/internal";
|
|
101
|
+
var StripeCheckoutSessionService = class {
|
|
102
|
+
constructor(stripeClient, em, cache, openTelemetryCollector, schemaValidator, mappers, options) {
|
|
103
|
+
this.stripeClient = stripeClient;
|
|
104
|
+
this.em = em;
|
|
105
|
+
this.cache = cache;
|
|
106
|
+
this.openTelemetryCollector = openTelemetryCollector;
|
|
107
|
+
this.schemaValidator = schemaValidator;
|
|
108
|
+
this.mappers = mappers;
|
|
109
|
+
this.options = options;
|
|
110
|
+
this._mappers = transformIntoInternalMapper2(mappers, schemaValidator);
|
|
111
|
+
this.baseCheckoutSessionService = new BaseCheckoutSessionService(
|
|
112
|
+
em,
|
|
113
|
+
cache,
|
|
114
|
+
openTelemetryCollector,
|
|
115
|
+
schemaValidator,
|
|
116
|
+
{
|
|
117
|
+
CheckoutSessionMapper: IdentityResponseMapper2,
|
|
118
|
+
CreateCheckoutSessionMapper: IdentityRequestMapper2,
|
|
119
|
+
UpdateCheckoutSessionMapper: IdentityRequestMapper2
|
|
120
|
+
},
|
|
121
|
+
options
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
baseCheckoutSessionService;
|
|
125
|
+
_mappers;
|
|
126
|
+
async createCheckoutSession(checkoutSessionDto) {
|
|
127
|
+
const session = await this.stripeClient.checkout.sessions.create({
|
|
128
|
+
...checkoutSessionDto.stripeFields,
|
|
129
|
+
payment_method_types: checkoutSessionDto.paymentMethods,
|
|
130
|
+
currency: checkoutSessionDto.currency,
|
|
131
|
+
success_url: checkoutSessionDto.successRedirectUri,
|
|
132
|
+
cancel_url: checkoutSessionDto.cancelRedirectUri
|
|
133
|
+
});
|
|
134
|
+
const checkoutSessionEntity = await this.baseCheckoutSessionService.createCheckoutSession(
|
|
135
|
+
await this._mappers.CreateCheckoutSessionMapper.deserializeDtoToEntity(
|
|
136
|
+
{
|
|
137
|
+
...checkoutSessionDto,
|
|
138
|
+
id: session.id,
|
|
139
|
+
uri: session.url,
|
|
140
|
+
expiresAt: new Date(Date.now() + 5 * 60 * 1e3),
|
|
141
|
+
providerFields: session
|
|
142
|
+
},
|
|
143
|
+
this.em,
|
|
144
|
+
session
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
return this._mappers.CheckoutSessionMapper.serializeEntityToDto(
|
|
148
|
+
checkoutSessionEntity
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
async getCheckoutSession({
|
|
152
|
+
id
|
|
153
|
+
}) {
|
|
154
|
+
const databaseCheckoutSession = await this.baseCheckoutSessionService.getCheckoutSession({ id });
|
|
155
|
+
return {
|
|
156
|
+
...this._mappers.CheckoutSessionMapper.serializeEntityToDto(
|
|
157
|
+
databaseCheckoutSession
|
|
158
|
+
),
|
|
159
|
+
stripeFields: await this.stripeClient.checkout.sessions.retrieve(id)
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async expireCheckoutSession({ id }) {
|
|
163
|
+
await this.stripeClient.checkout.sessions.expire(id);
|
|
164
|
+
await this.baseCheckoutSessionService.expireCheckoutSession({ id });
|
|
165
|
+
}
|
|
166
|
+
async handleCheckoutSuccess({ id }) {
|
|
167
|
+
await this.stripeClient.checkout.sessions.update(id, {
|
|
168
|
+
metadata: {
|
|
169
|
+
status: "SUCCESS"
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
await this.baseCheckoutSessionService.handleCheckoutSuccess({ id });
|
|
173
|
+
}
|
|
174
|
+
async handleCheckoutFailure({ id }) {
|
|
175
|
+
await this.stripeClient.checkout.sessions.update(id, {
|
|
176
|
+
metadata: {
|
|
177
|
+
status: "FAILED"
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
await this.baseCheckoutSessionService.handleCheckoutFailure({ id });
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// services/paymentLink.service.ts
|
|
185
|
+
import { BasePaymentLinkService } from "@forklaunch/implementation-billing-base/services";
|
|
186
|
+
import {
|
|
187
|
+
IdentityRequestMapper as IdentityRequestMapper3,
|
|
188
|
+
IdentityResponseMapper as IdentityResponseMapper3,
|
|
189
|
+
transformIntoInternalMapper as transformIntoInternalMapper3
|
|
190
|
+
} from "@forklaunch/internal";
|
|
191
|
+
var StripePaymentLinkService = class {
|
|
192
|
+
constructor(stripeClient, em, cache, openTelemetryCollector, schemaValidator, mappers, options) {
|
|
193
|
+
this.stripeClient = stripeClient;
|
|
194
|
+
this.em = em;
|
|
195
|
+
this.cache = cache;
|
|
196
|
+
this.openTelemetryCollector = openTelemetryCollector;
|
|
197
|
+
this.schemaValidator = schemaValidator;
|
|
198
|
+
this.mappers = mappers;
|
|
199
|
+
this.options = options;
|
|
200
|
+
this._mappers = transformIntoInternalMapper3(mappers, schemaValidator);
|
|
201
|
+
this.basePaymentLinkService = new BasePaymentLinkService(
|
|
202
|
+
em,
|
|
203
|
+
cache,
|
|
204
|
+
openTelemetryCollector,
|
|
205
|
+
schemaValidator,
|
|
206
|
+
{
|
|
207
|
+
PaymentLinkMapper: IdentityResponseMapper3,
|
|
208
|
+
CreatePaymentLinkMapper: IdentityRequestMapper3,
|
|
209
|
+
UpdatePaymentLinkMapper: IdentityRequestMapper3
|
|
210
|
+
},
|
|
211
|
+
options
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
basePaymentLinkService;
|
|
215
|
+
_mappers;
|
|
216
|
+
async createPaymentLink(paymentLinkDto) {
|
|
217
|
+
const session = await this.stripeClient.paymentLinks.create({
|
|
218
|
+
...paymentLinkDto.stripeFields,
|
|
219
|
+
payment_method_types: paymentLinkDto.paymentMethods,
|
|
220
|
+
currency: paymentLinkDto.currency
|
|
221
|
+
});
|
|
222
|
+
const paymentLinkEntity = await this.basePaymentLinkService.createPaymentLink(
|
|
223
|
+
await this._mappers.CreatePaymentLinkMapper.deserializeDtoToEntity(
|
|
224
|
+
{
|
|
225
|
+
...paymentLinkDto,
|
|
226
|
+
id: session.id,
|
|
227
|
+
amount: session.line_items?.data.reduce(
|
|
228
|
+
(total, item) => total + item.amount_total,
|
|
229
|
+
0
|
|
230
|
+
) ?? 0
|
|
231
|
+
},
|
|
232
|
+
this.em,
|
|
233
|
+
session
|
|
234
|
+
)
|
|
235
|
+
);
|
|
236
|
+
return this._mappers.PaymentLinkMapper.serializeEntityToDto(
|
|
237
|
+
paymentLinkEntity
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
async updatePaymentLink(paymentLinkDto) {
|
|
241
|
+
const session = await this.stripeClient.paymentLinks.update(
|
|
242
|
+
paymentLinkDto.id,
|
|
243
|
+
{
|
|
244
|
+
...paymentLinkDto.stripeFields,
|
|
245
|
+
payment_method_types: paymentLinkDto.paymentMethods
|
|
246
|
+
}
|
|
247
|
+
);
|
|
248
|
+
const paymentLinkEntity = await this.basePaymentLinkService.updatePaymentLink(
|
|
249
|
+
await this._mappers.UpdatePaymentLinkMapper.deserializeDtoToEntity(
|
|
250
|
+
{
|
|
251
|
+
...paymentLinkDto,
|
|
252
|
+
id: session.id,
|
|
253
|
+
amount: session.line_items?.data.reduce(
|
|
254
|
+
(total, item) => total + item.amount_total,
|
|
255
|
+
0
|
|
256
|
+
) ?? 0
|
|
257
|
+
},
|
|
258
|
+
this.em,
|
|
259
|
+
session
|
|
260
|
+
)
|
|
261
|
+
);
|
|
262
|
+
return this._mappers.PaymentLinkMapper.serializeEntityToDto(
|
|
263
|
+
paymentLinkEntity
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
async getPaymentLink({ id }) {
|
|
267
|
+
const databasePaymentLink = await this.basePaymentLinkService.getPaymentLink({ id });
|
|
268
|
+
return {
|
|
269
|
+
...this._mappers.PaymentLinkMapper.serializeEntityToDto(
|
|
270
|
+
databasePaymentLink
|
|
271
|
+
),
|
|
272
|
+
stripeFields: await this.stripeClient.paymentLinks.retrieve(id)
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
async expirePaymentLink({ id }) {
|
|
276
|
+
await this.stripeClient.paymentLinks.update(id, {
|
|
277
|
+
metadata: {
|
|
278
|
+
status: "EXPIRED"
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
await this.basePaymentLinkService.expirePaymentLink({ id });
|
|
282
|
+
}
|
|
283
|
+
async handlePaymentSuccess({ id }) {
|
|
284
|
+
await this.stripeClient.paymentLinks.update(id, {
|
|
285
|
+
metadata: {
|
|
286
|
+
status: "COMPLETED"
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
await this.basePaymentLinkService.handlePaymentSuccess({ id });
|
|
290
|
+
}
|
|
291
|
+
async handlePaymentFailure({ id }) {
|
|
292
|
+
await this.stripeClient.paymentLinks.update(id, {
|
|
293
|
+
metadata: {
|
|
294
|
+
status: "FAILED"
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
await this.basePaymentLinkService.handlePaymentFailure({ id });
|
|
298
|
+
}
|
|
299
|
+
async listPaymentLinks(idsDto) {
|
|
300
|
+
const paymentLinks = await this.stripeClient.paymentLinks.list({
|
|
301
|
+
active: true
|
|
302
|
+
});
|
|
303
|
+
return await Promise.all(
|
|
304
|
+
(await this.basePaymentLinkService.listPaymentLinks(idsDto)).map(
|
|
305
|
+
async (paymentLink) => ({
|
|
306
|
+
...await this._mappers.PaymentLinkMapper.serializeEntityToDto(
|
|
307
|
+
paymentLink
|
|
308
|
+
),
|
|
309
|
+
stripeFields: paymentLinks.data.find(
|
|
310
|
+
(paymentLink2) => paymentLink2.id === paymentLink2.id
|
|
311
|
+
)
|
|
312
|
+
})
|
|
313
|
+
)
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// services/plan.service.ts
|
|
319
|
+
import { BasePlanService } from "@forklaunch/implementation-billing-base/services";
|
|
320
|
+
import {
|
|
321
|
+
IdentityRequestMapper as IdentityRequestMapper4,
|
|
322
|
+
IdentityResponseMapper as IdentityResponseMapper4,
|
|
323
|
+
transformIntoInternalMapper as transformIntoInternalMapper4
|
|
324
|
+
} from "@forklaunch/internal";
|
|
325
|
+
var StripePlanService = class {
|
|
326
|
+
constructor(stripeClient, em, openTelemetryCollector, schemaValidator, mappers, options) {
|
|
327
|
+
this.stripeClient = stripeClient;
|
|
328
|
+
this.em = em;
|
|
329
|
+
this.openTelemetryCollector = openTelemetryCollector;
|
|
330
|
+
this.schemaValidator = schemaValidator;
|
|
331
|
+
this.mappers = mappers;
|
|
332
|
+
this.options = options;
|
|
333
|
+
this._mappers = transformIntoInternalMapper4(mappers, schemaValidator);
|
|
334
|
+
this.basePlanService = new BasePlanService(
|
|
335
|
+
em,
|
|
336
|
+
openTelemetryCollector,
|
|
337
|
+
schemaValidator,
|
|
338
|
+
{
|
|
339
|
+
PlanMapper: IdentityResponseMapper4,
|
|
340
|
+
CreatePlanMapper: IdentityRequestMapper4,
|
|
341
|
+
UpdatePlanMapper: IdentityRequestMapper4
|
|
342
|
+
},
|
|
343
|
+
options
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
basePlanService;
|
|
347
|
+
_mappers;
|
|
348
|
+
async createPlan(planDto, em) {
|
|
349
|
+
const plan = await this.stripeClient.plans.create({
|
|
350
|
+
...planDto.stripeFields,
|
|
351
|
+
interval: planDto.cadence,
|
|
352
|
+
product: planDto.name,
|
|
353
|
+
currency: planDto.currency
|
|
354
|
+
});
|
|
355
|
+
const planEntity = await this.basePlanService.createPlan(
|
|
356
|
+
await this._mappers.CreatePlanMapper.deserializeDtoToEntity(
|
|
357
|
+
{
|
|
358
|
+
...planDto,
|
|
359
|
+
externalId: plan.id,
|
|
360
|
+
billingProvider: "stripe"
|
|
361
|
+
},
|
|
362
|
+
em ?? this.em,
|
|
363
|
+
plan
|
|
364
|
+
),
|
|
365
|
+
em
|
|
366
|
+
);
|
|
367
|
+
return this._mappers.PlanMapper.serializeEntityToDto(planEntity);
|
|
368
|
+
}
|
|
369
|
+
async getPlan(idDto, em) {
|
|
370
|
+
const plan = await this.stripeClient.plans.retrieve(idDto.id);
|
|
371
|
+
const id = (await em?.findOne(
|
|
372
|
+
this.options?.databaseTableName ?? "plan",
|
|
373
|
+
{ externalId: idDto.id }
|
|
374
|
+
))?.id;
|
|
375
|
+
if (!id) {
|
|
376
|
+
throw new Error("Plan not found");
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
...await this._mappers.PlanMapper.serializeEntityToDto(
|
|
380
|
+
await this.basePlanService.getPlan({ id }, em)
|
|
381
|
+
),
|
|
382
|
+
stripeFields: plan
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
async updatePlan(planDto, em) {
|
|
386
|
+
const existingPlan = await this.stripeClient.plans.retrieve(planDto.id);
|
|
387
|
+
const plan = await this.stripeClient.plans.del(planDto.id).then(
|
|
388
|
+
() => this.stripeClient.plans.create({
|
|
389
|
+
...planDto.stripeFields,
|
|
390
|
+
interval: planDto.cadence ?? existingPlan.interval,
|
|
391
|
+
product: planDto.name,
|
|
392
|
+
currency: planDto.currency ?? existingPlan.currency
|
|
393
|
+
})
|
|
394
|
+
);
|
|
395
|
+
const planEntity = await this.basePlanService.updatePlan(
|
|
396
|
+
await this._mappers.UpdatePlanMapper.deserializeDtoToEntity(
|
|
397
|
+
{
|
|
398
|
+
...planDto,
|
|
399
|
+
externalId: plan.id,
|
|
400
|
+
billingProvider: "stripe"
|
|
401
|
+
},
|
|
402
|
+
em ?? this.em,
|
|
403
|
+
plan
|
|
404
|
+
),
|
|
405
|
+
em
|
|
406
|
+
);
|
|
407
|
+
return this._mappers.PlanMapper.serializeEntityToDto(planEntity);
|
|
408
|
+
}
|
|
409
|
+
async deletePlan(idDto, em) {
|
|
410
|
+
await this.stripeClient.plans.del(idDto.id);
|
|
411
|
+
await this.basePlanService.deletePlan(idDto, em);
|
|
412
|
+
}
|
|
413
|
+
async listPlans(idsDto, em) {
|
|
414
|
+
const plans = await this.stripeClient.plans.list({
|
|
415
|
+
active: true
|
|
416
|
+
});
|
|
417
|
+
const ids = (await em?.findAll(
|
|
418
|
+
this.options?.databaseTableName ?? "plan",
|
|
419
|
+
{ where: { externalId: { $in: plans.data.map((plan) => plan.id) } } }
|
|
420
|
+
))?.filter((s) => idsDto?.ids?.includes(s.id))?.map((s) => s.id);
|
|
421
|
+
if (!ids) {
|
|
422
|
+
throw new Error("Plans not found");
|
|
423
|
+
}
|
|
424
|
+
return await Promise.all(
|
|
425
|
+
(await this.basePlanService.listPlans({ ids }, em)).map(async (plan) => ({
|
|
426
|
+
...await this._mappers.PlanMapper.serializeEntityToDto(plan),
|
|
427
|
+
stripeFields: plans.data.find(
|
|
428
|
+
(stripePlan) => stripePlan.id === plan.externalId
|
|
429
|
+
)
|
|
430
|
+
}))
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
// services/subscription.service.ts
|
|
436
|
+
import { BaseSubscriptionService } from "@forklaunch/implementation-billing-base/services";
|
|
437
|
+
import {
|
|
438
|
+
IdentityRequestMapper as IdentityRequestMapper5,
|
|
439
|
+
IdentityResponseMapper as IdentityResponseMapper5,
|
|
440
|
+
transformIntoInternalMapper as transformIntoInternalMapper5
|
|
441
|
+
} from "@forklaunch/internal";
|
|
442
|
+
var StripeSubscriptionService = class {
|
|
443
|
+
constructor(stripe, em, openTelemetryCollector, schemaValidator, mappers, options) {
|
|
444
|
+
this.stripe = stripe;
|
|
445
|
+
this.em = em;
|
|
446
|
+
this.openTelemetryCollector = openTelemetryCollector;
|
|
447
|
+
this.schemaValidator = schemaValidator;
|
|
448
|
+
this.mappers = mappers;
|
|
449
|
+
this.options = options;
|
|
450
|
+
this._mappers = transformIntoInternalMapper5(mappers, schemaValidator);
|
|
451
|
+
this.baseSubscriptionService = new BaseSubscriptionService(
|
|
452
|
+
em,
|
|
453
|
+
openTelemetryCollector,
|
|
454
|
+
schemaValidator,
|
|
455
|
+
{
|
|
456
|
+
SubscriptionMapper: IdentityResponseMapper5,
|
|
457
|
+
CreateSubscriptionMapper: IdentityRequestMapper5,
|
|
458
|
+
UpdateSubscriptionMapper: IdentityRequestMapper5
|
|
459
|
+
},
|
|
460
|
+
options
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
baseSubscriptionService;
|
|
464
|
+
_mappers;
|
|
465
|
+
async createSubscription(subscriptionDto, em) {
|
|
466
|
+
const subscription = await this.stripe.subscriptions.create({
|
|
467
|
+
...subscriptionDto.stripeFields,
|
|
468
|
+
customer: subscriptionDto.partyId,
|
|
469
|
+
items: [
|
|
470
|
+
{
|
|
471
|
+
plan: subscriptionDto.productId
|
|
472
|
+
}
|
|
473
|
+
]
|
|
474
|
+
});
|
|
475
|
+
const subscriptionEntity = await this.baseSubscriptionService.createSubscription(
|
|
476
|
+
await this._mappers.CreateSubscriptionMapper.deserializeDtoToEntity(
|
|
477
|
+
{
|
|
478
|
+
...subscriptionDto,
|
|
479
|
+
externalId: subscription.id,
|
|
480
|
+
billingProvider: "stripe"
|
|
481
|
+
},
|
|
482
|
+
em ?? this.em,
|
|
483
|
+
subscription
|
|
484
|
+
),
|
|
485
|
+
em
|
|
486
|
+
);
|
|
487
|
+
return this._mappers.SubscriptionMapper.serializeEntityToDto(
|
|
488
|
+
subscriptionEntity
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
async getSubscription(idDto, em) {
|
|
492
|
+
return {
|
|
493
|
+
...await this._mappers.SubscriptionMapper.serializeEntityToDto(
|
|
494
|
+
await this.baseSubscriptionService.getSubscription(idDto, em)
|
|
495
|
+
),
|
|
496
|
+
stripeFields: await this.stripe.subscriptions.retrieve(idDto.id)
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
async getUserSubscription(idDto, em) {
|
|
500
|
+
return {
|
|
501
|
+
...await this._mappers.SubscriptionMapper.serializeEntityToDto(
|
|
502
|
+
await this.baseSubscriptionService.getUserSubscription(idDto, em)
|
|
503
|
+
),
|
|
504
|
+
stripeFields: await this.stripe.subscriptions.retrieve(idDto.id)
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
async getOrganizationSubscription(idDto, em) {
|
|
508
|
+
const id = (await em?.findOne(
|
|
509
|
+
this.options?.databaseTableName ?? "subscription",
|
|
510
|
+
{ externalId: idDto.id }
|
|
511
|
+
))?.id;
|
|
512
|
+
if (!id) {
|
|
513
|
+
throw new Error("Subscription not found");
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
...await this._mappers.SubscriptionMapper.serializeEntityToDto(
|
|
517
|
+
await this.baseSubscriptionService.getOrganizationSubscription(
|
|
518
|
+
{ id },
|
|
519
|
+
em
|
|
520
|
+
)
|
|
521
|
+
),
|
|
522
|
+
stripeFields: await this.stripe.subscriptions.retrieve(idDto.id)
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
async updateSubscription(subscriptionDto, em) {
|
|
526
|
+
const subscription = await this.stripe.subscriptions.update(
|
|
527
|
+
subscriptionDto.id,
|
|
528
|
+
{
|
|
529
|
+
...subscriptionDto.stripeFields,
|
|
530
|
+
items: [
|
|
531
|
+
{
|
|
532
|
+
plan: subscriptionDto.productId
|
|
533
|
+
}
|
|
534
|
+
]
|
|
535
|
+
}
|
|
536
|
+
);
|
|
537
|
+
const subscriptionEntity = await this.baseSubscriptionService.updateSubscription(
|
|
538
|
+
await this._mappers.UpdateSubscriptionMapper.deserializeDtoToEntity(
|
|
539
|
+
{
|
|
540
|
+
...subscriptionDto,
|
|
541
|
+
externalId: subscription.id,
|
|
542
|
+
billingProvider: "stripe",
|
|
543
|
+
providerFields: subscription
|
|
544
|
+
},
|
|
545
|
+
em ?? this.em,
|
|
546
|
+
subscription
|
|
547
|
+
),
|
|
548
|
+
em
|
|
549
|
+
);
|
|
550
|
+
return this._mappers.SubscriptionMapper.serializeEntityToDto(
|
|
551
|
+
subscriptionEntity
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
async deleteSubscription(idDto, em) {
|
|
555
|
+
await this.stripe.subscriptions.cancel(idDto.id);
|
|
556
|
+
await this.baseSubscriptionService.deleteSubscription(idDto, em);
|
|
557
|
+
}
|
|
558
|
+
async listSubscriptions(idsDto, em) {
|
|
559
|
+
const subscriptions = (await this.stripe.subscriptions.list({
|
|
560
|
+
status: "active"
|
|
561
|
+
})).data.filter((s) => idsDto.ids?.includes(s.id));
|
|
562
|
+
const ids = (await em?.findAll(
|
|
563
|
+
this.options?.databaseTableName ?? "subscription",
|
|
564
|
+
{ where: { externalId: { $in: subscriptions.map((s) => s.id) } } }
|
|
565
|
+
))?.map((s) => s.id);
|
|
566
|
+
if (!ids) {
|
|
567
|
+
throw new Error("Subscriptions not found");
|
|
568
|
+
}
|
|
569
|
+
return await Promise.all(
|
|
570
|
+
(await this.baseSubscriptionService.listSubscriptions({ ids }, em)).map(
|
|
571
|
+
async (subscription) => {
|
|
572
|
+
return {
|
|
573
|
+
...await this._mappers.SubscriptionMapper.serializeEntityToDto(
|
|
574
|
+
subscription
|
|
575
|
+
),
|
|
576
|
+
stripeFields: subscriptions.find(
|
|
577
|
+
(s) => s.id === subscription.externalId
|
|
578
|
+
)
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
)
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
async cancelSubscription(idDto, em) {
|
|
585
|
+
await this.stripe.subscriptions.cancel(idDto.id);
|
|
586
|
+
await this.baseSubscriptionService.cancelSubscription(idDto, em);
|
|
587
|
+
}
|
|
588
|
+
async resumeSubscription(idDto, em) {
|
|
589
|
+
await this.stripe.subscriptions.resume(idDto.id);
|
|
590
|
+
await this.baseSubscriptionService.resumeSubscription(idDto, em);
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
// services/webhook.service.ts
|
|
595
|
+
var StripeWebhookService = class {
|
|
596
|
+
constructor(stripeClient, em, schemaValidator, openTelemetryCollector, billingPortalService, checkoutSessionService, paymentLinkService, planService, subscriptionService) {
|
|
597
|
+
this.stripeClient = stripeClient;
|
|
598
|
+
this.em = em;
|
|
599
|
+
this.schemaValidator = schemaValidator;
|
|
600
|
+
this.openTelemetryCollector = openTelemetryCollector;
|
|
601
|
+
this.billingPortalService = billingPortalService;
|
|
602
|
+
this.checkoutSessionService = checkoutSessionService;
|
|
603
|
+
this.paymentLinkService = paymentLinkService;
|
|
604
|
+
this.planService = planService;
|
|
605
|
+
this.subscriptionService = subscriptionService;
|
|
606
|
+
}
|
|
607
|
+
async handleWebhookEvent(event) {
|
|
608
|
+
if (this.openTelemetryCollector) {
|
|
609
|
+
this.openTelemetryCollector.info("Handling webhook event", event);
|
|
610
|
+
}
|
|
611
|
+
const eventType = event.type;
|
|
612
|
+
switch (eventType) {
|
|
613
|
+
case "billing_portal.session.created": {
|
|
614
|
+
this.billingPortalService.baseBillingPortalService.createBillingPortalSession(
|
|
615
|
+
{
|
|
616
|
+
id: event.data.object.id,
|
|
617
|
+
customerId: event.data.object.customer,
|
|
618
|
+
expiresAt: new Date(event.data.object.created + 5 * 60 * 1e3),
|
|
619
|
+
uri: event.data.object.url,
|
|
620
|
+
providerFields: event.data.object
|
|
621
|
+
}
|
|
622
|
+
);
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
case "checkout.session.expired": {
|
|
626
|
+
this.checkoutSessionService.handleCheckoutFailure({
|
|
627
|
+
id: event.data.object.id
|
|
628
|
+
});
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
case "checkout.session.completed": {
|
|
632
|
+
this.checkoutSessionService.handleCheckoutSuccess({
|
|
633
|
+
id: event.data.object.id
|
|
634
|
+
});
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
case "payment_link.created":
|
|
638
|
+
{
|
|
639
|
+
this.paymentLinkService.basePaymentLinkService.createPaymentLink({
|
|
640
|
+
id: event.data.object.id,
|
|
641
|
+
amount: event.data.object.line_items?.data.reduce(
|
|
642
|
+
(total, item) => total + item.amount_total,
|
|
643
|
+
0
|
|
644
|
+
) ?? 0,
|
|
645
|
+
paymentMethods: event.data.object.payment_method_types,
|
|
646
|
+
status: "CREATED",
|
|
647
|
+
currency: event.data.object.currency,
|
|
648
|
+
providerFields: event.data.object
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
break;
|
|
652
|
+
case "payment_link.updated": {
|
|
653
|
+
this.paymentLinkService.basePaymentLinkService.updatePaymentLink({
|
|
654
|
+
id: event.data.object.id,
|
|
655
|
+
amount: event.data.object.line_items?.data.reduce(
|
|
656
|
+
(total, item) => total + item.amount_total,
|
|
657
|
+
0
|
|
658
|
+
) ?? 0,
|
|
659
|
+
paymentMethods: event.data.object.payment_method_types,
|
|
660
|
+
status: "UPDATED",
|
|
661
|
+
currency: event.data.object.currency,
|
|
662
|
+
providerFields: event.data.object
|
|
663
|
+
});
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
case "plan.created": {
|
|
667
|
+
if (typeof event.data.object.product === "object" && event.data.object.product != null && event.data.object.amount != null) {
|
|
668
|
+
this.planService.basePlanService.createPlan({
|
|
669
|
+
id: event.data.object.id,
|
|
670
|
+
billingProvider: "stripe" /* STRIPE */,
|
|
671
|
+
cadence: event.data.object.interval,
|
|
672
|
+
currency: event.data.object.currency,
|
|
673
|
+
active: true,
|
|
674
|
+
name: typeof event.data.object.product === "string" ? event.data.object.product : event.data.object.product?.id,
|
|
675
|
+
price: event.data.object.amount,
|
|
676
|
+
externalId: event.data.object.id,
|
|
677
|
+
providerFields: event.data.object
|
|
678
|
+
});
|
|
679
|
+
} else {
|
|
680
|
+
throw new Error("Invalid plan");
|
|
681
|
+
}
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
case "plan.updated": {
|
|
685
|
+
if (typeof event.data.object.product === "object" && event.data.object.product != null && event.data.object.amount != null) {
|
|
686
|
+
this.planService.basePlanService.updatePlan({
|
|
687
|
+
id: event.data.object.id,
|
|
688
|
+
billingProvider: "stripe" /* STRIPE */,
|
|
689
|
+
cadence: event.data.object.interval,
|
|
690
|
+
currency: event.data.object.currency,
|
|
691
|
+
active: true,
|
|
692
|
+
name: typeof event.data.object.product === "string" ? event.data.object.product : event.data.object.product?.id,
|
|
693
|
+
price: event.data.object.amount,
|
|
694
|
+
externalId: event.data.object.id,
|
|
695
|
+
providerFields: event.data.object
|
|
696
|
+
});
|
|
697
|
+
} else {
|
|
698
|
+
throw new Error("Invalid plan");
|
|
699
|
+
}
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
case "plan.deleted": {
|
|
703
|
+
this.planService.deletePlan({
|
|
704
|
+
id: event.data.object.id
|
|
705
|
+
});
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
case "customer.subscription.created": {
|
|
709
|
+
this.subscriptionService.baseSubscriptionService.createSubscription({
|
|
710
|
+
id: event.data.object.id,
|
|
711
|
+
partyId: typeof event.data.object.customer === "string" ? event.data.object.customer : event.data.object.customer.id,
|
|
712
|
+
partyType: "USER",
|
|
713
|
+
description: event.data.object.description ?? void 0,
|
|
714
|
+
active: true,
|
|
715
|
+
productId: event.data.object.items.data[0].plan.id,
|
|
716
|
+
providerFields: event.data.object,
|
|
717
|
+
externalId: event.data.object.id,
|
|
718
|
+
billingProvider: "stripe" /* STRIPE */,
|
|
719
|
+
startDate: new Date(event.data.object.created),
|
|
720
|
+
endDate: event.data.object.cancel_at ? new Date(event.data.object.cancel_at) : /* @__PURE__ */ new Date(Infinity),
|
|
721
|
+
status: event.data.object.status
|
|
722
|
+
});
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
case "customer.subscription.updated": {
|
|
726
|
+
this.subscriptionService.baseSubscriptionService.updateSubscription({
|
|
727
|
+
id: event.data.object.id,
|
|
728
|
+
partyId: typeof event.data.object.customer === "string" ? event.data.object.customer : event.data.object.customer.id,
|
|
729
|
+
partyType: "USER",
|
|
730
|
+
description: event.data.object.description ?? void 0,
|
|
731
|
+
active: true,
|
|
732
|
+
providerFields: event.data.object,
|
|
733
|
+
externalId: event.data.object.id,
|
|
734
|
+
billingProvider: "stripe" /* STRIPE */,
|
|
735
|
+
startDate: new Date(event.data.object.created),
|
|
736
|
+
endDate: event.data.object.cancel_at ? new Date(event.data.object.cancel_at) : /* @__PURE__ */ new Date(Infinity),
|
|
737
|
+
productId: event.data.object.items.data[0].plan.id,
|
|
738
|
+
status: event.data.object.status
|
|
739
|
+
});
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
case "customer.subscription.deleted": {
|
|
743
|
+
this.subscriptionService.deleteSubscription({
|
|
744
|
+
id: event.data.object.id
|
|
745
|
+
});
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
case "customer.subscription.paused": {
|
|
749
|
+
this.subscriptionService.cancelSubscription({
|
|
750
|
+
id: event.data.object.id
|
|
751
|
+
});
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
case "customer.subscription.resumed": {
|
|
755
|
+
this.subscriptionService.resumeSubscription({
|
|
756
|
+
id: event.data.object.id
|
|
757
|
+
});
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
default:
|
|
761
|
+
this.openTelemetryCollector.info(
|
|
762
|
+
"Unprocessed stripe event type",
|
|
763
|
+
eventType
|
|
764
|
+
);
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
export {
|
|
770
|
+
StripeBillingPortalService,
|
|
771
|
+
StripeCheckoutSessionService,
|
|
772
|
+
StripePaymentLinkService,
|
|
773
|
+
StripePlanService,
|
|
774
|
+
StripeSubscriptionService,
|
|
775
|
+
StripeWebhookService
|
|
776
|
+
};
|