@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.
@@ -0,0 +1,636 @@
1
+ // src/payments/definition.ts
2
+ import { defineService } from "@parsrun/service";
3
+ var paymentsServiceDefinition = defineService({
4
+ name: "payments",
5
+ version: "1.0.0",
6
+ description: "Payments, billing, and subscription management microservice",
7
+ queries: {
8
+ /**
9
+ * Get subscription details
10
+ */
11
+ getSubscription: {
12
+ input: void 0,
13
+ output: void 0,
14
+ description: "Get subscription details by ID or customer ID"
15
+ },
16
+ /**
17
+ * Get customer details
18
+ */
19
+ getCustomer: {
20
+ input: void 0,
21
+ output: void 0,
22
+ description: "Get customer details"
23
+ },
24
+ /**
25
+ * Check quota status
26
+ */
27
+ checkQuota: {
28
+ input: void 0,
29
+ output: void 0,
30
+ description: "Check if customer has quota for a feature"
31
+ },
32
+ /**
33
+ * Get usage summary
34
+ */
35
+ getUsage: {
36
+ input: void 0,
37
+ output: void 0,
38
+ description: "Get usage summary for a customer"
39
+ },
40
+ /**
41
+ * Get available plans
42
+ */
43
+ getPlans: {
44
+ input: void 0,
45
+ output: void 0,
46
+ description: "Get available subscription plans"
47
+ },
48
+ /**
49
+ * Get dunning status
50
+ */
51
+ getDunningStatus: {
52
+ input: void 0,
53
+ output: void 0,
54
+ description: "Get dunning status for a customer"
55
+ }
56
+ },
57
+ mutations: {
58
+ /**
59
+ * Create a checkout session
60
+ */
61
+ createCheckout: {
62
+ input: void 0,
63
+ output: void 0,
64
+ description: "Create a checkout session for subscription"
65
+ },
66
+ /**
67
+ * Cancel a subscription
68
+ */
69
+ cancelSubscription: {
70
+ input: void 0,
71
+ output: void 0,
72
+ description: "Cancel a subscription"
73
+ },
74
+ /**
75
+ * Update subscription
76
+ */
77
+ updateSubscription: {
78
+ input: void 0,
79
+ output: void 0,
80
+ description: "Update a subscription (e.g., change plan)"
81
+ },
82
+ /**
83
+ * Create customer portal session
84
+ */
85
+ createPortalSession: {
86
+ input: void 0,
87
+ output: void 0,
88
+ description: "Create a customer portal session for self-service"
89
+ },
90
+ /**
91
+ * Track usage
92
+ */
93
+ trackUsage: {
94
+ input: void 0,
95
+ output: void 0,
96
+ description: "Track usage of a metered feature"
97
+ },
98
+ /**
99
+ * Assign plan to customer
100
+ */
101
+ assignPlan: {
102
+ input: void 0,
103
+ output: void 0,
104
+ description: "Manually assign a plan to a customer"
105
+ },
106
+ /**
107
+ * Handle webhook
108
+ */
109
+ handleWebhook: {
110
+ input: void 0,
111
+ output: void 0,
112
+ description: "Handle payment provider webhook"
113
+ }
114
+ },
115
+ events: {
116
+ emits: {
117
+ /**
118
+ * Subscription was created
119
+ */
120
+ "subscription.created": {
121
+ data: void 0,
122
+ delivery: "at-least-once",
123
+ description: "A new subscription was created"
124
+ },
125
+ /**
126
+ * Subscription was renewed
127
+ */
128
+ "subscription.renewed": {
129
+ data: void 0,
130
+ delivery: "at-least-once",
131
+ description: "Subscription was renewed for a new period"
132
+ },
133
+ /**
134
+ * Subscription was canceled
135
+ */
136
+ "subscription.canceled": {
137
+ data: void 0,
138
+ delivery: "at-least-once",
139
+ description: "Subscription was canceled"
140
+ },
141
+ /**
142
+ * Subscription plan changed
143
+ */
144
+ "subscription.plan_changed": {
145
+ data: void 0,
146
+ delivery: "at-least-once",
147
+ description: "Subscription plan was changed"
148
+ },
149
+ /**
150
+ * Payment succeeded
151
+ */
152
+ "payment.succeeded": {
153
+ data: void 0,
154
+ delivery: "at-least-once",
155
+ description: "Payment was successful"
156
+ },
157
+ /**
158
+ * Payment failed
159
+ */
160
+ "payment.failed": {
161
+ data: void 0,
162
+ delivery: "at-least-once",
163
+ description: "Payment failed"
164
+ },
165
+ /**
166
+ * Quota exceeded
167
+ */
168
+ "quota.exceeded": {
169
+ data: void 0,
170
+ delivery: "at-least-once",
171
+ description: "Customer exceeded their quota for a feature"
172
+ },
173
+ /**
174
+ * Quota threshold reached
175
+ */
176
+ "quota.threshold_reached": {
177
+ data: void 0,
178
+ delivery: "at-least-once",
179
+ description: "Customer reached a usage threshold (e.g., 80%)"
180
+ },
181
+ /**
182
+ * Dunning started
183
+ */
184
+ "dunning.started": {
185
+ data: void 0,
186
+ delivery: "at-least-once",
187
+ description: "Dunning process started for a customer"
188
+ },
189
+ /**
190
+ * Dunning resolved
191
+ */
192
+ "dunning.resolved": {
193
+ data: void 0,
194
+ delivery: "at-least-once",
195
+ description: "Dunning process was resolved"
196
+ }
197
+ },
198
+ handles: [
199
+ // Events this service listens to
200
+ "user.created",
201
+ // Create customer record
202
+ "user.deleted",
203
+ // Cancel subscriptions
204
+ "tenant.suspended"
205
+ // Pause billing
206
+ ]
207
+ }
208
+ });
209
+
210
+ // src/payments/server.ts
211
+ import { createLogger } from "@parsrun/core";
212
+ import {
213
+ createRpcServer,
214
+ createEventEmitter,
215
+ createMemoryEventTransport,
216
+ getEmbeddedRegistry
217
+ } from "@parsrun/service";
218
+ var InMemoryPaymentsStorage = class {
219
+ usage = /* @__PURE__ */ new Map();
220
+ customerPlans = /* @__PURE__ */ new Map();
221
+ plans = [
222
+ {
223
+ id: "free",
224
+ name: "free",
225
+ displayName: "Free",
226
+ tier: 0,
227
+ basePrice: 0,
228
+ currency: "USD",
229
+ billingInterval: "month",
230
+ features: [
231
+ { featureKey: "api_calls", limitValue: 1e3, limitPeriod: "month" },
232
+ { featureKey: "storage_mb", limitValue: 100, limitPeriod: null }
233
+ ]
234
+ },
235
+ {
236
+ id: "pro",
237
+ name: "pro",
238
+ displayName: "Pro",
239
+ tier: 1,
240
+ basePrice: 2900,
241
+ currency: "USD",
242
+ billingInterval: "month",
243
+ features: [
244
+ { featureKey: "api_calls", limitValue: 1e5, limitPeriod: "month" },
245
+ { featureKey: "storage_mb", limitValue: 1e4, limitPeriod: null }
246
+ ]
247
+ },
248
+ {
249
+ id: "enterprise",
250
+ name: "enterprise",
251
+ displayName: "Enterprise",
252
+ tier: 2,
253
+ basePrice: 9900,
254
+ currency: "USD",
255
+ billingInterval: "month",
256
+ features: [
257
+ { featureKey: "api_calls", limitValue: null, limitPeriod: "month" },
258
+ { featureKey: "storage_mb", limitValue: null, limitPeriod: null }
259
+ ]
260
+ }
261
+ ];
262
+ async getUsage(customerId, featureKey) {
263
+ return this.usage.get(`${customerId}:${featureKey}`) ?? 0;
264
+ }
265
+ async trackUsage(customerId, featureKey, quantity) {
266
+ const key = `${customerId}:${featureKey}`;
267
+ const current = this.usage.get(key) ?? 0;
268
+ const newTotal = current + quantity;
269
+ this.usage.set(key, newTotal);
270
+ return newTotal;
271
+ }
272
+ async resetUsage(customerId, featureKey) {
273
+ if (featureKey) {
274
+ this.usage.delete(`${customerId}:${featureKey}`);
275
+ } else {
276
+ for (const key of this.usage.keys()) {
277
+ if (key.startsWith(`${customerId}:`)) {
278
+ this.usage.delete(key);
279
+ }
280
+ }
281
+ }
282
+ }
283
+ async getPlan(planId) {
284
+ return this.plans.find((p) => p.id === planId) ?? null;
285
+ }
286
+ async getPlans() {
287
+ return this.plans;
288
+ }
289
+ async getCustomerPlan(customerId) {
290
+ return this.customerPlans.get(customerId) ?? "free";
291
+ }
292
+ async setCustomerPlan(customerId, planId) {
293
+ this.customerPlans.set(customerId, planId);
294
+ }
295
+ };
296
+ function createPaymentsServiceServer(options) {
297
+ const logger = options.logger ?? createLogger({ name: "payments-service" });
298
+ const eventTransport = options.eventTransport ?? createMemoryEventTransport();
299
+ const storage = options.storage ?? new InMemoryPaymentsStorage();
300
+ const eventEmitter = createEventEmitter({
301
+ service: "payments",
302
+ definition: paymentsServiceDefinition,
303
+ transport: eventTransport,
304
+ logger
305
+ });
306
+ const handlers = {
307
+ queries: {
308
+ getSubscription: async (input, ctx) => {
309
+ const { subscriptionId, customerId } = input;
310
+ ctx.logger.debug("Getting subscription", { subscriptionId, customerId });
311
+ if (!subscriptionId && !customerId) {
312
+ return null;
313
+ }
314
+ return {
315
+ id: subscriptionId ?? `sub_${customerId}`,
316
+ customerId: customerId ?? "cus_123",
317
+ status: "active",
318
+ planId: "pro",
319
+ planName: "Pro",
320
+ currentPeriodStart: (/* @__PURE__ */ new Date()).toISOString(),
321
+ currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString(),
322
+ cancelAtPeriodEnd: false,
323
+ provider: options.providers.default.type
324
+ };
325
+ },
326
+ getCustomer: async (input, ctx) => {
327
+ const { customerId } = input;
328
+ ctx.logger.debug("Getting customer", { customerId });
329
+ return {
330
+ id: customerId,
331
+ email: `${customerId}@example.com`,
332
+ name: "Test Customer",
333
+ provider: options.providers.default.type
334
+ };
335
+ },
336
+ checkQuota: async (input, ctx) => {
337
+ const { customerId, featureKey } = input;
338
+ ctx.logger.debug("Checking quota", { customerId, featureKey });
339
+ const planId = await storage.getCustomerPlan(customerId);
340
+ const plan = planId ? await storage.getPlan(planId) : null;
341
+ const feature = plan?.features.find((f) => f.featureKey === featureKey);
342
+ const used = await storage.getUsage(customerId, featureKey);
343
+ const limit = feature?.limitValue ?? null;
344
+ const percentage = limit ? Math.round(used / limit * 100) : 0;
345
+ const allowed = limit === null || used < limit;
346
+ return {
347
+ allowed,
348
+ remaining: limit !== null ? Math.max(0, limit - used) : null,
349
+ limit,
350
+ percentage
351
+ };
352
+ },
353
+ getUsage: async (input, ctx) => {
354
+ const { customerId, featureKey } = input;
355
+ ctx.logger.debug("Getting usage", { customerId, featureKey });
356
+ const planId = await storage.getCustomerPlan(customerId);
357
+ const plan = planId ? await storage.getPlan(planId) : null;
358
+ const features = [];
359
+ const featuresToCheck = featureKey ? plan?.features.filter((f) => f.featureKey === featureKey) ?? [] : plan?.features ?? [];
360
+ for (const f of featuresToCheck) {
361
+ const used = await storage.getUsage(customerId, f.featureKey);
362
+ features.push({
363
+ featureKey: f.featureKey,
364
+ used,
365
+ limit: f.limitValue,
366
+ percentage: f.limitValue ? Math.round(used / f.limitValue * 100) : 0
367
+ });
368
+ }
369
+ return {
370
+ features,
371
+ period: {
372
+ start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3).toISOString(),
373
+ end: (/* @__PURE__ */ new Date()).toISOString()
374
+ }
375
+ };
376
+ },
377
+ getPlans: async (_input, ctx) => {
378
+ ctx.logger.debug("Getting plans");
379
+ const plans = await storage.getPlans();
380
+ return { plans };
381
+ },
382
+ getDunningStatus: async (input, ctx) => {
383
+ const { customerId } = input;
384
+ ctx.logger.debug("Getting dunning status", { customerId });
385
+ return { inDunning: false };
386
+ }
387
+ },
388
+ mutations: {
389
+ createCheckout: async (input, ctx) => {
390
+ const { email, planId, successUrl: _successUrl, cancelUrl: _cancelUrl, countryCode } = input;
391
+ void _successUrl;
392
+ void _cancelUrl;
393
+ ctx.logger.info("Creating checkout", { email, planId, countryCode });
394
+ const sessionId = `cs_${Date.now()}_${Math.random().toString(36).slice(2)}`;
395
+ const customerId = `cus_${Date.now()}`;
396
+ return {
397
+ checkoutUrl: `https://checkout.example.com/${sessionId}`,
398
+ sessionId,
399
+ customerId,
400
+ provider: options.providers.default.type
401
+ };
402
+ },
403
+ cancelSubscription: async (input, ctx) => {
404
+ const { subscriptionId, cancelAtPeriodEnd, reason } = input;
405
+ ctx.logger.info("Canceling subscription", { subscriptionId, reason });
406
+ await eventEmitter.emit("subscription.canceled", {
407
+ subscriptionId,
408
+ customerId: "cus_123",
409
+ reason,
410
+ effectiveAt: cancelAtPeriodEnd ? new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
411
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
412
+ });
413
+ return {
414
+ success: true,
415
+ canceledAt: (/* @__PURE__ */ new Date()).toISOString(),
416
+ effectiveAt: cancelAtPeriodEnd ? new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString() : (/* @__PURE__ */ new Date()).toISOString()
417
+ };
418
+ },
419
+ updateSubscription: async (input, ctx) => {
420
+ const { subscriptionId, planId } = input;
421
+ ctx.logger.info("Updating subscription", { subscriptionId, planId });
422
+ if (planId) {
423
+ await eventEmitter.emit("subscription.plan_changed", {
424
+ subscriptionId,
425
+ customerId: "cus_123",
426
+ previousPlanId: "pro",
427
+ newPlanId: planId,
428
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
429
+ });
430
+ }
431
+ return {
432
+ success: true,
433
+ subscription: {
434
+ id: subscriptionId,
435
+ planId: planId ?? "pro",
436
+ status: "active"
437
+ }
438
+ };
439
+ },
440
+ createPortalSession: async (input, ctx) => {
441
+ const { customerId, returnUrl: _returnUrl } = input;
442
+ void _returnUrl;
443
+ ctx.logger.info("Creating portal session", { customerId });
444
+ return {
445
+ portalUrl: `https://billing.example.com/portal/${customerId}`,
446
+ expiresAt: new Date(Date.now() + 60 * 60 * 1e3).toISOString()
447
+ };
448
+ },
449
+ trackUsage: async (input, ctx) => {
450
+ const { customerId, featureKey, quantity } = input;
451
+ ctx.logger.debug("Tracking usage", { customerId, featureKey, quantity });
452
+ const newTotal = await storage.trackUsage(customerId, featureKey, quantity);
453
+ const planId = await storage.getCustomerPlan(customerId);
454
+ const plan = planId ? await storage.getPlan(planId) : null;
455
+ const feature = plan?.features.find((f) => f.featureKey === featureKey);
456
+ const limit = feature?.limitValue ?? null;
457
+ if (limit !== null) {
458
+ const percentage = Math.round(newTotal / limit * 100);
459
+ if (percentage >= 100 && newTotal - quantity < limit) {
460
+ await eventEmitter.emit("quota.exceeded", {
461
+ customerId,
462
+ featureKey,
463
+ used: newTotal,
464
+ limit,
465
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
466
+ });
467
+ } else if (percentage >= 80 && Math.round((newTotal - quantity) / limit * 100) < 80) {
468
+ await eventEmitter.emit("quota.threshold_reached", {
469
+ customerId,
470
+ featureKey,
471
+ percentage,
472
+ used: newTotal,
473
+ limit,
474
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
475
+ });
476
+ }
477
+ }
478
+ return {
479
+ success: true,
480
+ newTotal,
481
+ remaining: limit !== null ? Math.max(0, limit - newTotal) : null
482
+ };
483
+ },
484
+ assignPlan: async (input, ctx) => {
485
+ const { customerId, planId } = input;
486
+ ctx.logger.info("Assigning plan", { customerId, planId });
487
+ const previousPlanId = await storage.getCustomerPlan(customerId);
488
+ await storage.setCustomerPlan(customerId, planId);
489
+ return {
490
+ success: true,
491
+ previousPlanId: previousPlanId ?? void 0,
492
+ newPlanId: planId
493
+ };
494
+ },
495
+ handleWebhook: async (input, ctx) => {
496
+ const { provider, payload: _payload, signature: _signature } = input;
497
+ void _payload;
498
+ void _signature;
499
+ ctx.logger.info("Handling webhook", { provider });
500
+ return {
501
+ success: true,
502
+ eventType: "payment.succeeded",
503
+ eventId: `evt_${Date.now()}`
504
+ };
505
+ }
506
+ }
507
+ };
508
+ const rpcServer = createRpcServer({
509
+ definition: paymentsServiceDefinition,
510
+ handlers,
511
+ logger
512
+ });
513
+ const register = () => {
514
+ const registry = getEmbeddedRegistry();
515
+ registry.register("payments", rpcServer);
516
+ logger.info("Payments service registered");
517
+ };
518
+ return {
519
+ rpcServer,
520
+ eventEmitter,
521
+ register
522
+ };
523
+ }
524
+
525
+ // src/payments/client.ts
526
+ import {
527
+ useService
528
+ } from "@parsrun/service";
529
+ function createPaymentsServiceClient(options) {
530
+ const client = useService("payments", options);
531
+ return {
532
+ // Queries
533
+ async getSubscription(opts) {
534
+ return client.query("getSubscription", opts);
535
+ },
536
+ async getCustomer(customerId) {
537
+ return client.query("getCustomer", { customerId });
538
+ },
539
+ async checkQuota(customerId, featureKey) {
540
+ return client.query("checkQuota", { customerId, featureKey });
541
+ },
542
+ async getUsage(opts) {
543
+ return client.query("getUsage", opts);
544
+ },
545
+ async getPlans() {
546
+ return client.query("getPlans", void 0);
547
+ },
548
+ async getDunningStatus(customerId) {
549
+ return client.query("getDunningStatus", { customerId });
550
+ },
551
+ // Mutations
552
+ async createCheckout(opts) {
553
+ return client.mutate("createCheckout", opts);
554
+ },
555
+ async cancelSubscription(opts) {
556
+ return client.mutate("cancelSubscription", opts);
557
+ },
558
+ async updateSubscription(opts) {
559
+ return client.mutate("updateSubscription", opts);
560
+ },
561
+ async createPortalSession(customerId, returnUrl) {
562
+ return client.mutate("createPortalSession", { customerId, returnUrl });
563
+ },
564
+ async trackUsage(opts) {
565
+ return client.mutate("trackUsage", opts);
566
+ },
567
+ async assignPlan(customerId, planId, expiresAt) {
568
+ return client.mutate("assignPlan", { customerId, planId, expiresAt });
569
+ },
570
+ async handleWebhook(opts) {
571
+ return client.mutate("handleWebhook", opts);
572
+ },
573
+ // Event subscriptions
574
+ onSubscriptionCreated(handler) {
575
+ return client.on("subscription.created", async (event) => {
576
+ await handler(event.data);
577
+ });
578
+ },
579
+ onSubscriptionRenewed(handler) {
580
+ return client.on("subscription.renewed", async (event) => {
581
+ await handler(event.data);
582
+ });
583
+ },
584
+ onSubscriptionCanceled(handler) {
585
+ return client.on("subscription.canceled", async (event) => {
586
+ await handler(event.data);
587
+ });
588
+ },
589
+ onSubscriptionPlanChanged(handler) {
590
+ return client.on("subscription.plan_changed", async (event) => {
591
+ await handler(event.data);
592
+ });
593
+ },
594
+ onPaymentSucceeded(handler) {
595
+ return client.on("payment.succeeded", async (event) => {
596
+ await handler(event.data);
597
+ });
598
+ },
599
+ onPaymentFailed(handler) {
600
+ return client.on("payment.failed", async (event) => {
601
+ await handler(event.data);
602
+ });
603
+ },
604
+ onQuotaExceeded(handler) {
605
+ return client.on("quota.exceeded", async (event) => {
606
+ await handler(event.data);
607
+ });
608
+ },
609
+ onQuotaThresholdReached(handler) {
610
+ return client.on("quota.threshold_reached", async (event) => {
611
+ await handler(event.data);
612
+ });
613
+ },
614
+ onDunningStarted(handler) {
615
+ return client.on("dunning.started", async (event) => {
616
+ await handler(event.data);
617
+ });
618
+ },
619
+ onDunningResolved(handler) {
620
+ return client.on("dunning.resolved", async (event) => {
621
+ await handler(event.data);
622
+ });
623
+ },
624
+ async close() {
625
+ if ("close" in client && typeof client.close === "function") {
626
+ await client.close();
627
+ }
628
+ }
629
+ };
630
+ }
631
+ export {
632
+ createPaymentsServiceClient,
633
+ createPaymentsServiceServer,
634
+ paymentsServiceDefinition
635
+ };
636
+ //# sourceMappingURL=index.js.map