@parsrun/service-adapters 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/README.md +166 -0
- package/dist/email/index.d.ts +306 -0
- package/dist/email/index.js +300 -0
- package/dist/email/index.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +934 -0
- package/dist/index.js.map +1 -0
- package/dist/payments/index.d.ts +782 -0
- package/dist/payments/index.js +636 -0
- package/dist/payments/index.js.map +1 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
// src/email/definition.ts
|
|
2
|
+
import { defineService } from "@parsrun/service";
|
|
3
|
+
var emailServiceDefinition = defineService({
|
|
4
|
+
name: "email",
|
|
5
|
+
version: "1.0.0",
|
|
6
|
+
description: "Email sending microservice",
|
|
7
|
+
queries: {
|
|
8
|
+
/**
|
|
9
|
+
* Check if email configuration is valid
|
|
10
|
+
*/
|
|
11
|
+
verify: {
|
|
12
|
+
input: void 0,
|
|
13
|
+
output: void 0,
|
|
14
|
+
description: "Verify email provider configuration"
|
|
15
|
+
},
|
|
16
|
+
/**
|
|
17
|
+
* Get available email templates
|
|
18
|
+
*/
|
|
19
|
+
getTemplates: {
|
|
20
|
+
input: void 0,
|
|
21
|
+
output: void 0,
|
|
22
|
+
description: "List available email templates"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
mutations: {
|
|
26
|
+
/**
|
|
27
|
+
* Send a single email
|
|
28
|
+
*/
|
|
29
|
+
send: {
|
|
30
|
+
input: void 0,
|
|
31
|
+
output: void 0,
|
|
32
|
+
description: "Send a single email"
|
|
33
|
+
},
|
|
34
|
+
/**
|
|
35
|
+
* Send batch emails
|
|
36
|
+
*/
|
|
37
|
+
sendBatch: {
|
|
38
|
+
input: void 0,
|
|
39
|
+
output: void 0,
|
|
40
|
+
description: "Send multiple emails in batch"
|
|
41
|
+
},
|
|
42
|
+
/**
|
|
43
|
+
* Render an email template
|
|
44
|
+
*/
|
|
45
|
+
renderTemplate: {
|
|
46
|
+
input: void 0,
|
|
47
|
+
output: void 0,
|
|
48
|
+
description: "Render an email template without sending"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
events: {
|
|
52
|
+
emits: {
|
|
53
|
+
/**
|
|
54
|
+
* Emitted when an email is sent successfully
|
|
55
|
+
*/
|
|
56
|
+
"email.sent": {
|
|
57
|
+
data: void 0,
|
|
58
|
+
delivery: "at-least-once",
|
|
59
|
+
description: "Email was sent successfully"
|
|
60
|
+
},
|
|
61
|
+
/**
|
|
62
|
+
* Emitted when an email fails to send
|
|
63
|
+
*/
|
|
64
|
+
"email.failed": {
|
|
65
|
+
data: void 0,
|
|
66
|
+
delivery: "at-least-once",
|
|
67
|
+
description: "Email failed to send"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
handles: [
|
|
71
|
+
// Events this service listens to
|
|
72
|
+
"user.created",
|
|
73
|
+
// Send welcome email
|
|
74
|
+
"user.password_reset_requested",
|
|
75
|
+
// Send password reset email
|
|
76
|
+
"user.email_verification_requested",
|
|
77
|
+
// Send verification email
|
|
78
|
+
"tenant.invitation_created",
|
|
79
|
+
// Send invitation email
|
|
80
|
+
"dunning.notification_required"
|
|
81
|
+
// Send dunning emails
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// src/email/server.ts
|
|
87
|
+
import { createLogger } from "@parsrun/core";
|
|
88
|
+
import {
|
|
89
|
+
createRpcServer,
|
|
90
|
+
createEventEmitter,
|
|
91
|
+
createMemoryEventTransport,
|
|
92
|
+
getEmbeddedRegistry
|
|
93
|
+
} from "@parsrun/service";
|
|
94
|
+
function createEmailServiceServer(options) {
|
|
95
|
+
const logger = options.logger ?? createLogger({ name: "email-service" });
|
|
96
|
+
const eventTransport = options.eventTransport ?? createMemoryEventTransport();
|
|
97
|
+
const eventEmitter = createEventEmitter({
|
|
98
|
+
service: "email",
|
|
99
|
+
definition: emailServiceDefinition,
|
|
100
|
+
transport: eventTransport,
|
|
101
|
+
logger
|
|
102
|
+
});
|
|
103
|
+
const handlers = {
|
|
104
|
+
queries: {
|
|
105
|
+
verify: async (_input, ctx) => {
|
|
106
|
+
ctx.logger.debug("Verifying email configuration");
|
|
107
|
+
return {
|
|
108
|
+
valid: !!options.provider.apiKey || options.provider.type === "console",
|
|
109
|
+
provider: options.provider.type
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
getTemplates: async (_input, ctx) => {
|
|
113
|
+
ctx.logger.debug("Getting available templates");
|
|
114
|
+
return {
|
|
115
|
+
templates: [
|
|
116
|
+
{
|
|
117
|
+
name: "welcome",
|
|
118
|
+
description: "Welcome email for new users",
|
|
119
|
+
variables: ["name", "loginUrl"]
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "password-reset",
|
|
123
|
+
description: "Password reset email",
|
|
124
|
+
variables: ["resetUrl", "expiresInMinutes"]
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "email-verification",
|
|
128
|
+
description: "Email verification link",
|
|
129
|
+
variables: ["verificationUrl", "expiresInHours"]
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "otp",
|
|
133
|
+
description: "One-time password email",
|
|
134
|
+
variables: ["code", "expiresInMinutes"]
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "magic-link",
|
|
138
|
+
description: "Magic link for passwordless login",
|
|
139
|
+
variables: ["url", "expiresInMinutes"]
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "invitation",
|
|
143
|
+
description: "Team/organization invitation",
|
|
144
|
+
variables: ["inviterName", "organizationName", "url", "role", "expiresInDays"]
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
mutations: {
|
|
151
|
+
send: async (input, ctx) => {
|
|
152
|
+
const { to, subject, html, text: _text, templateName, templateData: _templateData } = input;
|
|
153
|
+
void _text;
|
|
154
|
+
void _templateData;
|
|
155
|
+
ctx.logger.info("Sending email", { to, subject, templateName });
|
|
156
|
+
try {
|
|
157
|
+
const toArray = Array.isArray(to) ? to : [to];
|
|
158
|
+
const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
159
|
+
if (options.provider.type === "console") {
|
|
160
|
+
console.log("\u{1F4E7} Email sent:", {
|
|
161
|
+
to: toArray,
|
|
162
|
+
subject,
|
|
163
|
+
templateName,
|
|
164
|
+
html: html?.slice(0, 100)
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
await eventEmitter.emit("email.sent", {
|
|
168
|
+
messageId,
|
|
169
|
+
to: toArray,
|
|
170
|
+
subject,
|
|
171
|
+
provider: options.provider.type,
|
|
172
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
173
|
+
});
|
|
174
|
+
return { success: true, messageId };
|
|
175
|
+
} catch (error) {
|
|
176
|
+
const toArray = Array.isArray(to) ? to : [to];
|
|
177
|
+
await eventEmitter.emit("email.failed", {
|
|
178
|
+
to: toArray,
|
|
179
|
+
subject,
|
|
180
|
+
error: error.message,
|
|
181
|
+
provider: options.provider.type,
|
|
182
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
183
|
+
});
|
|
184
|
+
return { success: false, error: error.message };
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
sendBatch: async (input, ctx) => {
|
|
188
|
+
const { emails, stopOnError } = input;
|
|
189
|
+
ctx.logger.info("Sending batch emails", { count: emails.length });
|
|
190
|
+
const results = [];
|
|
191
|
+
let successful = 0;
|
|
192
|
+
let failed = 0;
|
|
193
|
+
for (const email of emails) {
|
|
194
|
+
try {
|
|
195
|
+
const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
196
|
+
if (options.provider.type === "console") {
|
|
197
|
+
console.log("\u{1F4E7} Batch email:", { to: email.to, subject: email.subject });
|
|
198
|
+
}
|
|
199
|
+
results.push({ success: true, messageId });
|
|
200
|
+
successful++;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
results.push({ success: false, error: error.message });
|
|
203
|
+
failed++;
|
|
204
|
+
if (stopOnError) {
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
total: emails.length,
|
|
211
|
+
successful,
|
|
212
|
+
failed,
|
|
213
|
+
results
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
renderTemplate: async (input, ctx) => {
|
|
217
|
+
const { templateName, data } = input;
|
|
218
|
+
ctx.logger.debug("Rendering template", { templateName });
|
|
219
|
+
const templates = {
|
|
220
|
+
welcome: {
|
|
221
|
+
subject: `Welcome to ${data["appName"] ?? "Our App"}!`,
|
|
222
|
+
html: `<h1>Welcome ${data["name"]}!</h1><p>Click <a href="${data["loginUrl"]}">here</a> to login.</p>`,
|
|
223
|
+
text: `Welcome ${data["name"]}! Visit ${data["loginUrl"]} to login.`
|
|
224
|
+
},
|
|
225
|
+
otp: {
|
|
226
|
+
subject: "Your verification code",
|
|
227
|
+
html: `<h1>Your code is: ${data["code"]}</h1><p>Expires in ${data["expiresInMinutes"]} minutes.</p>`,
|
|
228
|
+
text: `Your code is: ${data["code"]}. Expires in ${data["expiresInMinutes"]} minutes.`
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const template = templates[templateName];
|
|
232
|
+
if (!template) {
|
|
233
|
+
throw new Error(`Template not found: ${templateName}`);
|
|
234
|
+
}
|
|
235
|
+
return template;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const rpcServer = createRpcServer({
|
|
240
|
+
definition: emailServiceDefinition,
|
|
241
|
+
handlers,
|
|
242
|
+
logger
|
|
243
|
+
});
|
|
244
|
+
const register = () => {
|
|
245
|
+
const registry = getEmbeddedRegistry();
|
|
246
|
+
registry.register("email", rpcServer);
|
|
247
|
+
logger.info("Email service registered");
|
|
248
|
+
};
|
|
249
|
+
return {
|
|
250
|
+
rpcServer,
|
|
251
|
+
eventEmitter,
|
|
252
|
+
register
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/email/client.ts
|
|
257
|
+
import {
|
|
258
|
+
useService
|
|
259
|
+
} from "@parsrun/service";
|
|
260
|
+
function createEmailServiceClient(options) {
|
|
261
|
+
const client = useService("email", options);
|
|
262
|
+
return {
|
|
263
|
+
async verify() {
|
|
264
|
+
return client.query("verify", void 0);
|
|
265
|
+
},
|
|
266
|
+
async getTemplates() {
|
|
267
|
+
return client.query("getTemplates", void 0);
|
|
268
|
+
},
|
|
269
|
+
async send(emailOptions) {
|
|
270
|
+
return client.mutate("send", emailOptions);
|
|
271
|
+
},
|
|
272
|
+
async sendBatch(batchOptions) {
|
|
273
|
+
return client.mutate("sendBatch", batchOptions);
|
|
274
|
+
},
|
|
275
|
+
async renderTemplate(templateName, data) {
|
|
276
|
+
return client.mutate("renderTemplate", { templateName, data });
|
|
277
|
+
},
|
|
278
|
+
onEmailSent(handler) {
|
|
279
|
+
return client.on("email.sent", async (event) => {
|
|
280
|
+
await handler(event.data);
|
|
281
|
+
});
|
|
282
|
+
},
|
|
283
|
+
onEmailFailed(handler) {
|
|
284
|
+
return client.on("email.failed", async (event) => {
|
|
285
|
+
await handler(event.data);
|
|
286
|
+
});
|
|
287
|
+
},
|
|
288
|
+
async close() {
|
|
289
|
+
if ("close" in client && typeof client.close === "function") {
|
|
290
|
+
await client.close();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/payments/definition.ts
|
|
297
|
+
import { defineService as defineService2 } from "@parsrun/service";
|
|
298
|
+
var paymentsServiceDefinition = defineService2({
|
|
299
|
+
name: "payments",
|
|
300
|
+
version: "1.0.0",
|
|
301
|
+
description: "Payments, billing, and subscription management microservice",
|
|
302
|
+
queries: {
|
|
303
|
+
/**
|
|
304
|
+
* Get subscription details
|
|
305
|
+
*/
|
|
306
|
+
getSubscription: {
|
|
307
|
+
input: void 0,
|
|
308
|
+
output: void 0,
|
|
309
|
+
description: "Get subscription details by ID or customer ID"
|
|
310
|
+
},
|
|
311
|
+
/**
|
|
312
|
+
* Get customer details
|
|
313
|
+
*/
|
|
314
|
+
getCustomer: {
|
|
315
|
+
input: void 0,
|
|
316
|
+
output: void 0,
|
|
317
|
+
description: "Get customer details"
|
|
318
|
+
},
|
|
319
|
+
/**
|
|
320
|
+
* Check quota status
|
|
321
|
+
*/
|
|
322
|
+
checkQuota: {
|
|
323
|
+
input: void 0,
|
|
324
|
+
output: void 0,
|
|
325
|
+
description: "Check if customer has quota for a feature"
|
|
326
|
+
},
|
|
327
|
+
/**
|
|
328
|
+
* Get usage summary
|
|
329
|
+
*/
|
|
330
|
+
getUsage: {
|
|
331
|
+
input: void 0,
|
|
332
|
+
output: void 0,
|
|
333
|
+
description: "Get usage summary for a customer"
|
|
334
|
+
},
|
|
335
|
+
/**
|
|
336
|
+
* Get available plans
|
|
337
|
+
*/
|
|
338
|
+
getPlans: {
|
|
339
|
+
input: void 0,
|
|
340
|
+
output: void 0,
|
|
341
|
+
description: "Get available subscription plans"
|
|
342
|
+
},
|
|
343
|
+
/**
|
|
344
|
+
* Get dunning status
|
|
345
|
+
*/
|
|
346
|
+
getDunningStatus: {
|
|
347
|
+
input: void 0,
|
|
348
|
+
output: void 0,
|
|
349
|
+
description: "Get dunning status for a customer"
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
mutations: {
|
|
353
|
+
/**
|
|
354
|
+
* Create a checkout session
|
|
355
|
+
*/
|
|
356
|
+
createCheckout: {
|
|
357
|
+
input: void 0,
|
|
358
|
+
output: void 0,
|
|
359
|
+
description: "Create a checkout session for subscription"
|
|
360
|
+
},
|
|
361
|
+
/**
|
|
362
|
+
* Cancel a subscription
|
|
363
|
+
*/
|
|
364
|
+
cancelSubscription: {
|
|
365
|
+
input: void 0,
|
|
366
|
+
output: void 0,
|
|
367
|
+
description: "Cancel a subscription"
|
|
368
|
+
},
|
|
369
|
+
/**
|
|
370
|
+
* Update subscription
|
|
371
|
+
*/
|
|
372
|
+
updateSubscription: {
|
|
373
|
+
input: void 0,
|
|
374
|
+
output: void 0,
|
|
375
|
+
description: "Update a subscription (e.g., change plan)"
|
|
376
|
+
},
|
|
377
|
+
/**
|
|
378
|
+
* Create customer portal session
|
|
379
|
+
*/
|
|
380
|
+
createPortalSession: {
|
|
381
|
+
input: void 0,
|
|
382
|
+
output: void 0,
|
|
383
|
+
description: "Create a customer portal session for self-service"
|
|
384
|
+
},
|
|
385
|
+
/**
|
|
386
|
+
* Track usage
|
|
387
|
+
*/
|
|
388
|
+
trackUsage: {
|
|
389
|
+
input: void 0,
|
|
390
|
+
output: void 0,
|
|
391
|
+
description: "Track usage of a metered feature"
|
|
392
|
+
},
|
|
393
|
+
/**
|
|
394
|
+
* Assign plan to customer
|
|
395
|
+
*/
|
|
396
|
+
assignPlan: {
|
|
397
|
+
input: void 0,
|
|
398
|
+
output: void 0,
|
|
399
|
+
description: "Manually assign a plan to a customer"
|
|
400
|
+
},
|
|
401
|
+
/**
|
|
402
|
+
* Handle webhook
|
|
403
|
+
*/
|
|
404
|
+
handleWebhook: {
|
|
405
|
+
input: void 0,
|
|
406
|
+
output: void 0,
|
|
407
|
+
description: "Handle payment provider webhook"
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
events: {
|
|
411
|
+
emits: {
|
|
412
|
+
/**
|
|
413
|
+
* Subscription was created
|
|
414
|
+
*/
|
|
415
|
+
"subscription.created": {
|
|
416
|
+
data: void 0,
|
|
417
|
+
delivery: "at-least-once",
|
|
418
|
+
description: "A new subscription was created"
|
|
419
|
+
},
|
|
420
|
+
/**
|
|
421
|
+
* Subscription was renewed
|
|
422
|
+
*/
|
|
423
|
+
"subscription.renewed": {
|
|
424
|
+
data: void 0,
|
|
425
|
+
delivery: "at-least-once",
|
|
426
|
+
description: "Subscription was renewed for a new period"
|
|
427
|
+
},
|
|
428
|
+
/**
|
|
429
|
+
* Subscription was canceled
|
|
430
|
+
*/
|
|
431
|
+
"subscription.canceled": {
|
|
432
|
+
data: void 0,
|
|
433
|
+
delivery: "at-least-once",
|
|
434
|
+
description: "Subscription was canceled"
|
|
435
|
+
},
|
|
436
|
+
/**
|
|
437
|
+
* Subscription plan changed
|
|
438
|
+
*/
|
|
439
|
+
"subscription.plan_changed": {
|
|
440
|
+
data: void 0,
|
|
441
|
+
delivery: "at-least-once",
|
|
442
|
+
description: "Subscription plan was changed"
|
|
443
|
+
},
|
|
444
|
+
/**
|
|
445
|
+
* Payment succeeded
|
|
446
|
+
*/
|
|
447
|
+
"payment.succeeded": {
|
|
448
|
+
data: void 0,
|
|
449
|
+
delivery: "at-least-once",
|
|
450
|
+
description: "Payment was successful"
|
|
451
|
+
},
|
|
452
|
+
/**
|
|
453
|
+
* Payment failed
|
|
454
|
+
*/
|
|
455
|
+
"payment.failed": {
|
|
456
|
+
data: void 0,
|
|
457
|
+
delivery: "at-least-once",
|
|
458
|
+
description: "Payment failed"
|
|
459
|
+
},
|
|
460
|
+
/**
|
|
461
|
+
* Quota exceeded
|
|
462
|
+
*/
|
|
463
|
+
"quota.exceeded": {
|
|
464
|
+
data: void 0,
|
|
465
|
+
delivery: "at-least-once",
|
|
466
|
+
description: "Customer exceeded their quota for a feature"
|
|
467
|
+
},
|
|
468
|
+
/**
|
|
469
|
+
* Quota threshold reached
|
|
470
|
+
*/
|
|
471
|
+
"quota.threshold_reached": {
|
|
472
|
+
data: void 0,
|
|
473
|
+
delivery: "at-least-once",
|
|
474
|
+
description: "Customer reached a usage threshold (e.g., 80%)"
|
|
475
|
+
},
|
|
476
|
+
/**
|
|
477
|
+
* Dunning started
|
|
478
|
+
*/
|
|
479
|
+
"dunning.started": {
|
|
480
|
+
data: void 0,
|
|
481
|
+
delivery: "at-least-once",
|
|
482
|
+
description: "Dunning process started for a customer"
|
|
483
|
+
},
|
|
484
|
+
/**
|
|
485
|
+
* Dunning resolved
|
|
486
|
+
*/
|
|
487
|
+
"dunning.resolved": {
|
|
488
|
+
data: void 0,
|
|
489
|
+
delivery: "at-least-once",
|
|
490
|
+
description: "Dunning process was resolved"
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
handles: [
|
|
494
|
+
// Events this service listens to
|
|
495
|
+
"user.created",
|
|
496
|
+
// Create customer record
|
|
497
|
+
"user.deleted",
|
|
498
|
+
// Cancel subscriptions
|
|
499
|
+
"tenant.suspended"
|
|
500
|
+
// Pause billing
|
|
501
|
+
]
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// src/payments/server.ts
|
|
506
|
+
import { createLogger as createLogger2 } from "@parsrun/core";
|
|
507
|
+
import {
|
|
508
|
+
createRpcServer as createRpcServer2,
|
|
509
|
+
createEventEmitter as createEventEmitter2,
|
|
510
|
+
createMemoryEventTransport as createMemoryEventTransport2,
|
|
511
|
+
getEmbeddedRegistry as getEmbeddedRegistry2
|
|
512
|
+
} from "@parsrun/service";
|
|
513
|
+
var InMemoryPaymentsStorage = class {
|
|
514
|
+
usage = /* @__PURE__ */ new Map();
|
|
515
|
+
customerPlans = /* @__PURE__ */ new Map();
|
|
516
|
+
plans = [
|
|
517
|
+
{
|
|
518
|
+
id: "free",
|
|
519
|
+
name: "free",
|
|
520
|
+
displayName: "Free",
|
|
521
|
+
tier: 0,
|
|
522
|
+
basePrice: 0,
|
|
523
|
+
currency: "USD",
|
|
524
|
+
billingInterval: "month",
|
|
525
|
+
features: [
|
|
526
|
+
{ featureKey: "api_calls", limitValue: 1e3, limitPeriod: "month" },
|
|
527
|
+
{ featureKey: "storage_mb", limitValue: 100, limitPeriod: null }
|
|
528
|
+
]
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
id: "pro",
|
|
532
|
+
name: "pro",
|
|
533
|
+
displayName: "Pro",
|
|
534
|
+
tier: 1,
|
|
535
|
+
basePrice: 2900,
|
|
536
|
+
currency: "USD",
|
|
537
|
+
billingInterval: "month",
|
|
538
|
+
features: [
|
|
539
|
+
{ featureKey: "api_calls", limitValue: 1e5, limitPeriod: "month" },
|
|
540
|
+
{ featureKey: "storage_mb", limitValue: 1e4, limitPeriod: null }
|
|
541
|
+
]
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
id: "enterprise",
|
|
545
|
+
name: "enterprise",
|
|
546
|
+
displayName: "Enterprise",
|
|
547
|
+
tier: 2,
|
|
548
|
+
basePrice: 9900,
|
|
549
|
+
currency: "USD",
|
|
550
|
+
billingInterval: "month",
|
|
551
|
+
features: [
|
|
552
|
+
{ featureKey: "api_calls", limitValue: null, limitPeriod: "month" },
|
|
553
|
+
{ featureKey: "storage_mb", limitValue: null, limitPeriod: null }
|
|
554
|
+
]
|
|
555
|
+
}
|
|
556
|
+
];
|
|
557
|
+
async getUsage(customerId, featureKey) {
|
|
558
|
+
return this.usage.get(`${customerId}:${featureKey}`) ?? 0;
|
|
559
|
+
}
|
|
560
|
+
async trackUsage(customerId, featureKey, quantity) {
|
|
561
|
+
const key = `${customerId}:${featureKey}`;
|
|
562
|
+
const current = this.usage.get(key) ?? 0;
|
|
563
|
+
const newTotal = current + quantity;
|
|
564
|
+
this.usage.set(key, newTotal);
|
|
565
|
+
return newTotal;
|
|
566
|
+
}
|
|
567
|
+
async resetUsage(customerId, featureKey) {
|
|
568
|
+
if (featureKey) {
|
|
569
|
+
this.usage.delete(`${customerId}:${featureKey}`);
|
|
570
|
+
} else {
|
|
571
|
+
for (const key of this.usage.keys()) {
|
|
572
|
+
if (key.startsWith(`${customerId}:`)) {
|
|
573
|
+
this.usage.delete(key);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
async getPlan(planId) {
|
|
579
|
+
return this.plans.find((p) => p.id === planId) ?? null;
|
|
580
|
+
}
|
|
581
|
+
async getPlans() {
|
|
582
|
+
return this.plans;
|
|
583
|
+
}
|
|
584
|
+
async getCustomerPlan(customerId) {
|
|
585
|
+
return this.customerPlans.get(customerId) ?? "free";
|
|
586
|
+
}
|
|
587
|
+
async setCustomerPlan(customerId, planId) {
|
|
588
|
+
this.customerPlans.set(customerId, planId);
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
function createPaymentsServiceServer(options) {
|
|
592
|
+
const logger = options.logger ?? createLogger2({ name: "payments-service" });
|
|
593
|
+
const eventTransport = options.eventTransport ?? createMemoryEventTransport2();
|
|
594
|
+
const storage = options.storage ?? new InMemoryPaymentsStorage();
|
|
595
|
+
const eventEmitter = createEventEmitter2({
|
|
596
|
+
service: "payments",
|
|
597
|
+
definition: paymentsServiceDefinition,
|
|
598
|
+
transport: eventTransport,
|
|
599
|
+
logger
|
|
600
|
+
});
|
|
601
|
+
const handlers = {
|
|
602
|
+
queries: {
|
|
603
|
+
getSubscription: async (input, ctx) => {
|
|
604
|
+
const { subscriptionId, customerId } = input;
|
|
605
|
+
ctx.logger.debug("Getting subscription", { subscriptionId, customerId });
|
|
606
|
+
if (!subscriptionId && !customerId) {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
id: subscriptionId ?? `sub_${customerId}`,
|
|
611
|
+
customerId: customerId ?? "cus_123",
|
|
612
|
+
status: "active",
|
|
613
|
+
planId: "pro",
|
|
614
|
+
planName: "Pro",
|
|
615
|
+
currentPeriodStart: (/* @__PURE__ */ new Date()).toISOString(),
|
|
616
|
+
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString(),
|
|
617
|
+
cancelAtPeriodEnd: false,
|
|
618
|
+
provider: options.providers.default.type
|
|
619
|
+
};
|
|
620
|
+
},
|
|
621
|
+
getCustomer: async (input, ctx) => {
|
|
622
|
+
const { customerId } = input;
|
|
623
|
+
ctx.logger.debug("Getting customer", { customerId });
|
|
624
|
+
return {
|
|
625
|
+
id: customerId,
|
|
626
|
+
email: `${customerId}@example.com`,
|
|
627
|
+
name: "Test Customer",
|
|
628
|
+
provider: options.providers.default.type
|
|
629
|
+
};
|
|
630
|
+
},
|
|
631
|
+
checkQuota: async (input, ctx) => {
|
|
632
|
+
const { customerId, featureKey } = input;
|
|
633
|
+
ctx.logger.debug("Checking quota", { customerId, featureKey });
|
|
634
|
+
const planId = await storage.getCustomerPlan(customerId);
|
|
635
|
+
const plan = planId ? await storage.getPlan(planId) : null;
|
|
636
|
+
const feature = plan?.features.find((f) => f.featureKey === featureKey);
|
|
637
|
+
const used = await storage.getUsage(customerId, featureKey);
|
|
638
|
+
const limit = feature?.limitValue ?? null;
|
|
639
|
+
const percentage = limit ? Math.round(used / limit * 100) : 0;
|
|
640
|
+
const allowed = limit === null || used < limit;
|
|
641
|
+
return {
|
|
642
|
+
allowed,
|
|
643
|
+
remaining: limit !== null ? Math.max(0, limit - used) : null,
|
|
644
|
+
limit,
|
|
645
|
+
percentage
|
|
646
|
+
};
|
|
647
|
+
},
|
|
648
|
+
getUsage: async (input, ctx) => {
|
|
649
|
+
const { customerId, featureKey } = input;
|
|
650
|
+
ctx.logger.debug("Getting usage", { customerId, featureKey });
|
|
651
|
+
const planId = await storage.getCustomerPlan(customerId);
|
|
652
|
+
const plan = planId ? await storage.getPlan(planId) : null;
|
|
653
|
+
const features = [];
|
|
654
|
+
const featuresToCheck = featureKey ? plan?.features.filter((f) => f.featureKey === featureKey) ?? [] : plan?.features ?? [];
|
|
655
|
+
for (const f of featuresToCheck) {
|
|
656
|
+
const used = await storage.getUsage(customerId, f.featureKey);
|
|
657
|
+
features.push({
|
|
658
|
+
featureKey: f.featureKey,
|
|
659
|
+
used,
|
|
660
|
+
limit: f.limitValue,
|
|
661
|
+
percentage: f.limitValue ? Math.round(used / f.limitValue * 100) : 0
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
features,
|
|
666
|
+
period: {
|
|
667
|
+
start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3).toISOString(),
|
|
668
|
+
end: (/* @__PURE__ */ new Date()).toISOString()
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
},
|
|
672
|
+
getPlans: async (_input, ctx) => {
|
|
673
|
+
ctx.logger.debug("Getting plans");
|
|
674
|
+
const plans = await storage.getPlans();
|
|
675
|
+
return { plans };
|
|
676
|
+
},
|
|
677
|
+
getDunningStatus: async (input, ctx) => {
|
|
678
|
+
const { customerId } = input;
|
|
679
|
+
ctx.logger.debug("Getting dunning status", { customerId });
|
|
680
|
+
return { inDunning: false };
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
mutations: {
|
|
684
|
+
createCheckout: async (input, ctx) => {
|
|
685
|
+
const { email, planId, successUrl: _successUrl, cancelUrl: _cancelUrl, countryCode } = input;
|
|
686
|
+
void _successUrl;
|
|
687
|
+
void _cancelUrl;
|
|
688
|
+
ctx.logger.info("Creating checkout", { email, planId, countryCode });
|
|
689
|
+
const sessionId = `cs_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
690
|
+
const customerId = `cus_${Date.now()}`;
|
|
691
|
+
return {
|
|
692
|
+
checkoutUrl: `https://checkout.example.com/${sessionId}`,
|
|
693
|
+
sessionId,
|
|
694
|
+
customerId,
|
|
695
|
+
provider: options.providers.default.type
|
|
696
|
+
};
|
|
697
|
+
},
|
|
698
|
+
cancelSubscription: async (input, ctx) => {
|
|
699
|
+
const { subscriptionId, cancelAtPeriodEnd, reason } = input;
|
|
700
|
+
ctx.logger.info("Canceling subscription", { subscriptionId, reason });
|
|
701
|
+
await eventEmitter.emit("subscription.canceled", {
|
|
702
|
+
subscriptionId,
|
|
703
|
+
customerId: "cus_123",
|
|
704
|
+
reason,
|
|
705
|
+
effectiveAt: cancelAtPeriodEnd ? new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
706
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
707
|
+
});
|
|
708
|
+
return {
|
|
709
|
+
success: true,
|
|
710
|
+
canceledAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
711
|
+
effectiveAt: cancelAtPeriodEnd ? new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString() : (/* @__PURE__ */ new Date()).toISOString()
|
|
712
|
+
};
|
|
713
|
+
},
|
|
714
|
+
updateSubscription: async (input, ctx) => {
|
|
715
|
+
const { subscriptionId, planId } = input;
|
|
716
|
+
ctx.logger.info("Updating subscription", { subscriptionId, planId });
|
|
717
|
+
if (planId) {
|
|
718
|
+
await eventEmitter.emit("subscription.plan_changed", {
|
|
719
|
+
subscriptionId,
|
|
720
|
+
customerId: "cus_123",
|
|
721
|
+
previousPlanId: "pro",
|
|
722
|
+
newPlanId: planId,
|
|
723
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
success: true,
|
|
728
|
+
subscription: {
|
|
729
|
+
id: subscriptionId,
|
|
730
|
+
planId: planId ?? "pro",
|
|
731
|
+
status: "active"
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
},
|
|
735
|
+
createPortalSession: async (input, ctx) => {
|
|
736
|
+
const { customerId, returnUrl: _returnUrl } = input;
|
|
737
|
+
void _returnUrl;
|
|
738
|
+
ctx.logger.info("Creating portal session", { customerId });
|
|
739
|
+
return {
|
|
740
|
+
portalUrl: `https://billing.example.com/portal/${customerId}`,
|
|
741
|
+
expiresAt: new Date(Date.now() + 60 * 60 * 1e3).toISOString()
|
|
742
|
+
};
|
|
743
|
+
},
|
|
744
|
+
trackUsage: async (input, ctx) => {
|
|
745
|
+
const { customerId, featureKey, quantity } = input;
|
|
746
|
+
ctx.logger.debug("Tracking usage", { customerId, featureKey, quantity });
|
|
747
|
+
const newTotal = await storage.trackUsage(customerId, featureKey, quantity);
|
|
748
|
+
const planId = await storage.getCustomerPlan(customerId);
|
|
749
|
+
const plan = planId ? await storage.getPlan(planId) : null;
|
|
750
|
+
const feature = plan?.features.find((f) => f.featureKey === featureKey);
|
|
751
|
+
const limit = feature?.limitValue ?? null;
|
|
752
|
+
if (limit !== null) {
|
|
753
|
+
const percentage = Math.round(newTotal / limit * 100);
|
|
754
|
+
if (percentage >= 100 && newTotal - quantity < limit) {
|
|
755
|
+
await eventEmitter.emit("quota.exceeded", {
|
|
756
|
+
customerId,
|
|
757
|
+
featureKey,
|
|
758
|
+
used: newTotal,
|
|
759
|
+
limit,
|
|
760
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
761
|
+
});
|
|
762
|
+
} else if (percentage >= 80 && Math.round((newTotal - quantity) / limit * 100) < 80) {
|
|
763
|
+
await eventEmitter.emit("quota.threshold_reached", {
|
|
764
|
+
customerId,
|
|
765
|
+
featureKey,
|
|
766
|
+
percentage,
|
|
767
|
+
used: newTotal,
|
|
768
|
+
limit,
|
|
769
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return {
|
|
774
|
+
success: true,
|
|
775
|
+
newTotal,
|
|
776
|
+
remaining: limit !== null ? Math.max(0, limit - newTotal) : null
|
|
777
|
+
};
|
|
778
|
+
},
|
|
779
|
+
assignPlan: async (input, ctx) => {
|
|
780
|
+
const { customerId, planId } = input;
|
|
781
|
+
ctx.logger.info("Assigning plan", { customerId, planId });
|
|
782
|
+
const previousPlanId = await storage.getCustomerPlan(customerId);
|
|
783
|
+
await storage.setCustomerPlan(customerId, planId);
|
|
784
|
+
return {
|
|
785
|
+
success: true,
|
|
786
|
+
previousPlanId: previousPlanId ?? void 0,
|
|
787
|
+
newPlanId: planId
|
|
788
|
+
};
|
|
789
|
+
},
|
|
790
|
+
handleWebhook: async (input, ctx) => {
|
|
791
|
+
const { provider, payload: _payload, signature: _signature } = input;
|
|
792
|
+
void _payload;
|
|
793
|
+
void _signature;
|
|
794
|
+
ctx.logger.info("Handling webhook", { provider });
|
|
795
|
+
return {
|
|
796
|
+
success: true,
|
|
797
|
+
eventType: "payment.succeeded",
|
|
798
|
+
eventId: `evt_${Date.now()}`
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
const rpcServer = createRpcServer2({
|
|
804
|
+
definition: paymentsServiceDefinition,
|
|
805
|
+
handlers,
|
|
806
|
+
logger
|
|
807
|
+
});
|
|
808
|
+
const register = () => {
|
|
809
|
+
const registry = getEmbeddedRegistry2();
|
|
810
|
+
registry.register("payments", rpcServer);
|
|
811
|
+
logger.info("Payments service registered");
|
|
812
|
+
};
|
|
813
|
+
return {
|
|
814
|
+
rpcServer,
|
|
815
|
+
eventEmitter,
|
|
816
|
+
register
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// src/payments/client.ts
|
|
821
|
+
import {
|
|
822
|
+
useService as useService2
|
|
823
|
+
} from "@parsrun/service";
|
|
824
|
+
function createPaymentsServiceClient(options) {
|
|
825
|
+
const client = useService2("payments", options);
|
|
826
|
+
return {
|
|
827
|
+
// Queries
|
|
828
|
+
async getSubscription(opts) {
|
|
829
|
+
return client.query("getSubscription", opts);
|
|
830
|
+
},
|
|
831
|
+
async getCustomer(customerId) {
|
|
832
|
+
return client.query("getCustomer", { customerId });
|
|
833
|
+
},
|
|
834
|
+
async checkQuota(customerId, featureKey) {
|
|
835
|
+
return client.query("checkQuota", { customerId, featureKey });
|
|
836
|
+
},
|
|
837
|
+
async getUsage(opts) {
|
|
838
|
+
return client.query("getUsage", opts);
|
|
839
|
+
},
|
|
840
|
+
async getPlans() {
|
|
841
|
+
return client.query("getPlans", void 0);
|
|
842
|
+
},
|
|
843
|
+
async getDunningStatus(customerId) {
|
|
844
|
+
return client.query("getDunningStatus", { customerId });
|
|
845
|
+
},
|
|
846
|
+
// Mutations
|
|
847
|
+
async createCheckout(opts) {
|
|
848
|
+
return client.mutate("createCheckout", opts);
|
|
849
|
+
},
|
|
850
|
+
async cancelSubscription(opts) {
|
|
851
|
+
return client.mutate("cancelSubscription", opts);
|
|
852
|
+
},
|
|
853
|
+
async updateSubscription(opts) {
|
|
854
|
+
return client.mutate("updateSubscription", opts);
|
|
855
|
+
},
|
|
856
|
+
async createPortalSession(customerId, returnUrl) {
|
|
857
|
+
return client.mutate("createPortalSession", { customerId, returnUrl });
|
|
858
|
+
},
|
|
859
|
+
async trackUsage(opts) {
|
|
860
|
+
return client.mutate("trackUsage", opts);
|
|
861
|
+
},
|
|
862
|
+
async assignPlan(customerId, planId, expiresAt) {
|
|
863
|
+
return client.mutate("assignPlan", { customerId, planId, expiresAt });
|
|
864
|
+
},
|
|
865
|
+
async handleWebhook(opts) {
|
|
866
|
+
return client.mutate("handleWebhook", opts);
|
|
867
|
+
},
|
|
868
|
+
// Event subscriptions
|
|
869
|
+
onSubscriptionCreated(handler) {
|
|
870
|
+
return client.on("subscription.created", async (event) => {
|
|
871
|
+
await handler(event.data);
|
|
872
|
+
});
|
|
873
|
+
},
|
|
874
|
+
onSubscriptionRenewed(handler) {
|
|
875
|
+
return client.on("subscription.renewed", async (event) => {
|
|
876
|
+
await handler(event.data);
|
|
877
|
+
});
|
|
878
|
+
},
|
|
879
|
+
onSubscriptionCanceled(handler) {
|
|
880
|
+
return client.on("subscription.canceled", async (event) => {
|
|
881
|
+
await handler(event.data);
|
|
882
|
+
});
|
|
883
|
+
},
|
|
884
|
+
onSubscriptionPlanChanged(handler) {
|
|
885
|
+
return client.on("subscription.plan_changed", async (event) => {
|
|
886
|
+
await handler(event.data);
|
|
887
|
+
});
|
|
888
|
+
},
|
|
889
|
+
onPaymentSucceeded(handler) {
|
|
890
|
+
return client.on("payment.succeeded", async (event) => {
|
|
891
|
+
await handler(event.data);
|
|
892
|
+
});
|
|
893
|
+
},
|
|
894
|
+
onPaymentFailed(handler) {
|
|
895
|
+
return client.on("payment.failed", async (event) => {
|
|
896
|
+
await handler(event.data);
|
|
897
|
+
});
|
|
898
|
+
},
|
|
899
|
+
onQuotaExceeded(handler) {
|
|
900
|
+
return client.on("quota.exceeded", async (event) => {
|
|
901
|
+
await handler(event.data);
|
|
902
|
+
});
|
|
903
|
+
},
|
|
904
|
+
onQuotaThresholdReached(handler) {
|
|
905
|
+
return client.on("quota.threshold_reached", async (event) => {
|
|
906
|
+
await handler(event.data);
|
|
907
|
+
});
|
|
908
|
+
},
|
|
909
|
+
onDunningStarted(handler) {
|
|
910
|
+
return client.on("dunning.started", async (event) => {
|
|
911
|
+
await handler(event.data);
|
|
912
|
+
});
|
|
913
|
+
},
|
|
914
|
+
onDunningResolved(handler) {
|
|
915
|
+
return client.on("dunning.resolved", async (event) => {
|
|
916
|
+
await handler(event.data);
|
|
917
|
+
});
|
|
918
|
+
},
|
|
919
|
+
async close() {
|
|
920
|
+
if ("close" in client && typeof client.close === "function") {
|
|
921
|
+
await client.close();
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
export {
|
|
927
|
+
createEmailServiceClient,
|
|
928
|
+
createEmailServiceServer,
|
|
929
|
+
createPaymentsServiceClient,
|
|
930
|
+
createPaymentsServiceServer,
|
|
931
|
+
emailServiceDefinition,
|
|
932
|
+
paymentsServiceDefinition
|
|
933
|
+
};
|
|
934
|
+
//# sourceMappingURL=index.js.map
|