billsdk 0.2.0 → 0.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/dist/index.js +423 -21
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createDefaultTimeProvider } from '@billsdk/core';
|
|
1
2
|
export * from '@billsdk/core';
|
|
2
3
|
export { drizzleAdapter } from '@billsdk/drizzle-adapter';
|
|
3
4
|
import { memoryAdapter } from '@billsdk/memory-adapter';
|
|
@@ -325,7 +326,8 @@ function calculateProration(params) {
|
|
|
325
326
|
changeDate = /* @__PURE__ */ new Date()
|
|
326
327
|
} = params;
|
|
327
328
|
const totalDays = daysBetween(currentPeriodStart, currentPeriodEnd);
|
|
328
|
-
const
|
|
329
|
+
const daysUsed = daysBetween(currentPeriodStart, changeDate);
|
|
330
|
+
const daysRemaining = totalDays - daysUsed;
|
|
329
331
|
const effectiveDaysRemaining = Math.max(
|
|
330
332
|
0,
|
|
331
333
|
Math.min(daysRemaining, totalDays)
|
|
@@ -333,15 +335,13 @@ function calculateProration(params) {
|
|
|
333
335
|
const credit = Math.round(
|
|
334
336
|
oldPlanAmount / totalDays * effectiveDaysRemaining
|
|
335
337
|
);
|
|
336
|
-
const charge =
|
|
337
|
-
|
|
338
|
-
);
|
|
339
|
-
const netAmount = charge - credit;
|
|
338
|
+
const charge = newPlanAmount;
|
|
339
|
+
const netAmount = Math.max(0, charge - credit);
|
|
340
340
|
return {
|
|
341
341
|
credit,
|
|
342
342
|
charge,
|
|
343
343
|
netAmount,
|
|
344
|
-
|
|
344
|
+
daysUsed: Math.max(0, Math.min(daysUsed, totalDays)),
|
|
345
345
|
totalDays
|
|
346
346
|
};
|
|
347
347
|
}
|
|
@@ -490,8 +490,19 @@ async function cancelSubscription(ctx, params) {
|
|
|
490
490
|
accessUntil: subscription.currentPeriodEnd
|
|
491
491
|
};
|
|
492
492
|
}
|
|
493
|
+
var intervalRank = {
|
|
494
|
+
monthly: 1,
|
|
495
|
+
quarterly: 2,
|
|
496
|
+
yearly: 3
|
|
497
|
+
};
|
|
498
|
+
function isUpgrade(oldPrice, newPrice, oldInterval, newInterval) {
|
|
499
|
+
if (oldInterval !== newInterval) {
|
|
500
|
+
return intervalRank[newInterval] > intervalRank[oldInterval];
|
|
501
|
+
}
|
|
502
|
+
return newPrice > oldPrice;
|
|
503
|
+
}
|
|
493
504
|
async function changeSubscription(ctx, params) {
|
|
494
|
-
const { customerId, newPlanCode, prorate = true } = params;
|
|
505
|
+
const { customerId, newPlanCode, newInterval, prorate = true } = params;
|
|
495
506
|
const customer = await ctx.internalAdapter.findCustomerByExternalId(customerId);
|
|
496
507
|
if (!customer) {
|
|
497
508
|
throw new Error("Customer not found");
|
|
@@ -502,7 +513,8 @@ async function changeSubscription(ctx, params) {
|
|
|
502
513
|
if (!subscription) {
|
|
503
514
|
throw new Error("No active subscription found");
|
|
504
515
|
}
|
|
505
|
-
|
|
516
|
+
const targetInterval = newInterval ?? subscription.interval;
|
|
517
|
+
if (subscription.planCode === newPlanCode && subscription.interval === targetInterval) {
|
|
506
518
|
throw new Error("Already on this plan");
|
|
507
519
|
}
|
|
508
520
|
const oldPlan = ctx.internalAdapter.findPlanByCode(subscription.planCode);
|
|
@@ -519,27 +531,67 @@ async function changeSubscription(ctx, params) {
|
|
|
519
531
|
}
|
|
520
532
|
const newPrice = ctx.internalAdapter.getPlanPrice(
|
|
521
533
|
newPlanCode,
|
|
522
|
-
|
|
534
|
+
targetInterval
|
|
523
535
|
);
|
|
524
536
|
if (!newPrice) {
|
|
525
537
|
throw new Error(
|
|
526
|
-
`No price found for plan ${newPlanCode} with interval ${
|
|
538
|
+
`No price found for plan ${newPlanCode} with interval ${targetInterval}`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
const upgrade = isUpgrade(
|
|
542
|
+
oldPrice.amount,
|
|
543
|
+
newPrice.amount,
|
|
544
|
+
subscription.interval,
|
|
545
|
+
targetInterval
|
|
546
|
+
);
|
|
547
|
+
ctx.logger.info("Plan change detected", {
|
|
548
|
+
from: `${subscription.planCode} (${subscription.interval})`,
|
|
549
|
+
to: `${newPlanCode} (${targetInterval})`,
|
|
550
|
+
oldAmount: oldPrice.amount,
|
|
551
|
+
newAmount: newPrice.amount,
|
|
552
|
+
isUpgrade: upgrade
|
|
553
|
+
});
|
|
554
|
+
if (!upgrade) {
|
|
555
|
+
ctx.logger.info("Downgrade scheduled for period end", {
|
|
556
|
+
scheduledPlanCode: newPlanCode,
|
|
557
|
+
scheduledInterval: targetInterval,
|
|
558
|
+
effectiveAt: subscription.currentPeriodEnd
|
|
559
|
+
});
|
|
560
|
+
const updatedSubscription2 = await ctx.internalAdapter.updateSubscription(
|
|
561
|
+
subscription.id,
|
|
562
|
+
{
|
|
563
|
+
scheduledPlanCode: newPlanCode,
|
|
564
|
+
scheduledInterval: targetInterval
|
|
565
|
+
}
|
|
527
566
|
);
|
|
567
|
+
return {
|
|
568
|
+
subscription: updatedSubscription2,
|
|
569
|
+
previousPlan: oldPlan,
|
|
570
|
+
newPlan,
|
|
571
|
+
payment: null,
|
|
572
|
+
scheduled: true,
|
|
573
|
+
effectiveAt: subscription.currentPeriodEnd
|
|
574
|
+
};
|
|
528
575
|
}
|
|
529
576
|
let payment = null;
|
|
577
|
+
const changeDate = await ctx.timeProvider.now(params.customerId);
|
|
578
|
+
ctx.logger.info("changeDate from timeProvider", {
|
|
579
|
+
changeDate: changeDate.toISOString(),
|
|
580
|
+
realTime: (/* @__PURE__ */ new Date()).toISOString()
|
|
581
|
+
});
|
|
530
582
|
if (prorate) {
|
|
531
583
|
const prorationResult = calculateProration({
|
|
532
584
|
oldPlanAmount: oldPrice.amount,
|
|
533
585
|
newPlanAmount: newPrice.amount,
|
|
534
586
|
currentPeriodStart: subscription.currentPeriodStart,
|
|
535
587
|
currentPeriodEnd: subscription.currentPeriodEnd,
|
|
536
|
-
changeDate
|
|
588
|
+
changeDate
|
|
537
589
|
});
|
|
538
590
|
ctx.logger.info("Proration calculated", {
|
|
539
591
|
credit: prorationResult.credit,
|
|
540
592
|
charge: prorationResult.charge,
|
|
541
593
|
netAmount: prorationResult.netAmount,
|
|
542
|
-
|
|
594
|
+
daysUsed: prorationResult.daysUsed
|
|
543
595
|
});
|
|
544
596
|
if (prorationResult.netAmount > 0) {
|
|
545
597
|
if (!ctx.paymentAdapter?.charge) {
|
|
@@ -591,15 +643,36 @@ async function changeSubscription(ctx, params) {
|
|
|
591
643
|
});
|
|
592
644
|
}
|
|
593
645
|
}
|
|
646
|
+
const newPeriodStart = changeDate;
|
|
647
|
+
const newPeriodEnd = new Date(changeDate);
|
|
648
|
+
if (targetInterval === "yearly") {
|
|
649
|
+
newPeriodEnd.setFullYear(newPeriodEnd.getFullYear() + 1);
|
|
650
|
+
} else if (targetInterval === "quarterly") {
|
|
651
|
+
newPeriodEnd.setMonth(newPeriodEnd.getMonth() + 3);
|
|
652
|
+
} else {
|
|
653
|
+
newPeriodEnd.setMonth(newPeriodEnd.getMonth() + 1);
|
|
654
|
+
}
|
|
594
655
|
const updatedSubscription = await ctx.internalAdapter.updateSubscription(
|
|
595
656
|
subscription.id,
|
|
596
|
-
{
|
|
657
|
+
{
|
|
658
|
+
planCode: newPlanCode,
|
|
659
|
+
interval: targetInterval,
|
|
660
|
+
currentPeriodStart: newPeriodStart,
|
|
661
|
+
currentPeriodEnd: newPeriodEnd,
|
|
662
|
+
scheduledPlanCode: void 0,
|
|
663
|
+
scheduledInterval: void 0
|
|
664
|
+
}
|
|
597
665
|
);
|
|
666
|
+
ctx.logger.info("Period reset", {
|
|
667
|
+
from: `${subscription.currentPeriodStart.toISOString()} - ${subscription.currentPeriodEnd.toISOString()}`,
|
|
668
|
+
to: `${newPeriodStart.toISOString()} - ${newPeriodEnd.toISOString()}`
|
|
669
|
+
});
|
|
598
670
|
return {
|
|
599
671
|
subscription: updatedSubscription,
|
|
600
672
|
previousPlan: oldPlan,
|
|
601
673
|
newPlan,
|
|
602
|
-
payment
|
|
674
|
+
payment,
|
|
675
|
+
scheduled: false
|
|
603
676
|
};
|
|
604
677
|
}
|
|
605
678
|
|
|
@@ -762,6 +835,326 @@ var refundEndpoints = {
|
|
|
762
835
|
}
|
|
763
836
|
}
|
|
764
837
|
};
|
|
838
|
+
|
|
839
|
+
// src/logic/renewal-service.ts
|
|
840
|
+
function calculateNewPeriodEnd(from, interval) {
|
|
841
|
+
const newEnd = new Date(from);
|
|
842
|
+
if (interval === "yearly") {
|
|
843
|
+
newEnd.setFullYear(newEnd.getFullYear() + 1);
|
|
844
|
+
} else if (interval === "quarterly") {
|
|
845
|
+
newEnd.setMonth(newEnd.getMonth() + 3);
|
|
846
|
+
} else {
|
|
847
|
+
newEnd.setMonth(newEnd.getMonth() + 1);
|
|
848
|
+
}
|
|
849
|
+
return newEnd;
|
|
850
|
+
}
|
|
851
|
+
async function findDueSubscriptions(ctx, params) {
|
|
852
|
+
const now = params.customerId ? await ctx.timeProvider.now(params.customerId) : /* @__PURE__ */ new Date();
|
|
853
|
+
const where = [
|
|
854
|
+
// Only active or past_due subscriptions
|
|
855
|
+
{
|
|
856
|
+
field: "status",
|
|
857
|
+
operator: "in",
|
|
858
|
+
value: ["active", "past_due"]
|
|
859
|
+
},
|
|
860
|
+
// Period has ended
|
|
861
|
+
{
|
|
862
|
+
field: "currentPeriodEnd",
|
|
863
|
+
operator: "lte",
|
|
864
|
+
value: now
|
|
865
|
+
}
|
|
866
|
+
];
|
|
867
|
+
if (params.customerId) {
|
|
868
|
+
const customer = await ctx.internalAdapter.findCustomerByExternalId(
|
|
869
|
+
params.customerId
|
|
870
|
+
);
|
|
871
|
+
if (!customer) {
|
|
872
|
+
return [];
|
|
873
|
+
}
|
|
874
|
+
where.push({
|
|
875
|
+
field: "customerId",
|
|
876
|
+
operator: "eq",
|
|
877
|
+
value: customer.id
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
return ctx.adapter.findMany({
|
|
881
|
+
model: TABLES.SUBSCRIPTION,
|
|
882
|
+
where,
|
|
883
|
+
limit: params.limit,
|
|
884
|
+
sortBy: { field: "currentPeriodEnd", direction: "asc" }
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
async function applyScheduledChanges(ctx, subscription) {
|
|
888
|
+
if (!subscription.scheduledPlanCode) {
|
|
889
|
+
return {
|
|
890
|
+
planChanged: false,
|
|
891
|
+
newPlanCode: subscription.planCode,
|
|
892
|
+
newInterval: subscription.interval
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
const newPlanCode = subscription.scheduledPlanCode;
|
|
896
|
+
const newInterval = subscription.scheduledInterval ?? subscription.interval;
|
|
897
|
+
ctx.logger.info("Applying scheduled plan change", {
|
|
898
|
+
subscriptionId: subscription.id,
|
|
899
|
+
from: subscription.planCode,
|
|
900
|
+
to: newPlanCode,
|
|
901
|
+
newInterval
|
|
902
|
+
});
|
|
903
|
+
await ctx.internalAdapter.updateSubscription(subscription.id, {
|
|
904
|
+
planCode: newPlanCode,
|
|
905
|
+
interval: newInterval,
|
|
906
|
+
scheduledPlanCode: void 0,
|
|
907
|
+
scheduledInterval: void 0
|
|
908
|
+
});
|
|
909
|
+
return {
|
|
910
|
+
planChanged: true,
|
|
911
|
+
newPlanCode,
|
|
912
|
+
newInterval
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
async function processSubscriptionRenewal(ctx, subscription, dryRun) {
|
|
916
|
+
const customer = await ctx.internalAdapter.findCustomerById(
|
|
917
|
+
subscription.customerId
|
|
918
|
+
);
|
|
919
|
+
if (!customer) {
|
|
920
|
+
return {
|
|
921
|
+
subscriptionId: subscription.id,
|
|
922
|
+
customerId: subscription.customerId,
|
|
923
|
+
status: "failed",
|
|
924
|
+
error: "Customer not found"
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
const {
|
|
928
|
+
planChanged,
|
|
929
|
+
newPlanCode,
|
|
930
|
+
newInterval: newIntervalStr
|
|
931
|
+
} = await applyScheduledChanges(ctx, subscription);
|
|
932
|
+
const newInterval = newIntervalStr;
|
|
933
|
+
const price = ctx.internalAdapter.getPlanPrice(newPlanCode, newInterval);
|
|
934
|
+
if (!price) {
|
|
935
|
+
return {
|
|
936
|
+
subscriptionId: subscription.id,
|
|
937
|
+
customerId: customer.externalId,
|
|
938
|
+
status: "failed",
|
|
939
|
+
error: `No price found for plan ${newPlanCode} with interval ${newInterval}`,
|
|
940
|
+
planChanged: planChanged ? { from: subscription.planCode, to: newPlanCode } : void 0
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
const amount = price.amount;
|
|
944
|
+
const now = await ctx.timeProvider.now(customer.externalId);
|
|
945
|
+
if (dryRun) {
|
|
946
|
+
return {
|
|
947
|
+
subscriptionId: subscription.id,
|
|
948
|
+
customerId: customer.externalId,
|
|
949
|
+
status: "succeeded",
|
|
950
|
+
amount,
|
|
951
|
+
planChanged: planChanged ? { from: subscription.planCode, to: newPlanCode } : void 0
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
if (amount === 0) {
|
|
955
|
+
const newPeriodEnd2 = calculateNewPeriodEnd(now, newInterval);
|
|
956
|
+
await ctx.internalAdapter.updateSubscription(subscription.id, {
|
|
957
|
+
currentPeriodStart: now,
|
|
958
|
+
currentPeriodEnd: newPeriodEnd2,
|
|
959
|
+
status: "active"
|
|
960
|
+
});
|
|
961
|
+
ctx.logger.info("Free renewal processed", {
|
|
962
|
+
subscriptionId: subscription.id,
|
|
963
|
+
newPeriodEnd: newPeriodEnd2.toISOString()
|
|
964
|
+
});
|
|
965
|
+
return {
|
|
966
|
+
subscriptionId: subscription.id,
|
|
967
|
+
customerId: customer.externalId,
|
|
968
|
+
status: "succeeded",
|
|
969
|
+
amount: 0,
|
|
970
|
+
planChanged: planChanged ? { from: subscription.planCode, to: newPlanCode } : void 0
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
if (!customer.providerCustomerId) {
|
|
974
|
+
ctx.logger.warn("Customer has no payment method, marking as past_due", {
|
|
975
|
+
subscriptionId: subscription.id,
|
|
976
|
+
customerId: customer.externalId
|
|
977
|
+
});
|
|
978
|
+
await ctx.internalAdapter.updateSubscription(subscription.id, {
|
|
979
|
+
status: "past_due"
|
|
980
|
+
});
|
|
981
|
+
return {
|
|
982
|
+
subscriptionId: subscription.id,
|
|
983
|
+
customerId: customer.externalId,
|
|
984
|
+
status: "failed",
|
|
985
|
+
error: "No payment method on file",
|
|
986
|
+
planChanged: planChanged ? { from: subscription.planCode, to: newPlanCode } : void 0
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
if (!ctx.paymentAdapter?.charge) {
|
|
990
|
+
ctx.logger.error("Payment adapter does not support direct charging", {
|
|
991
|
+
subscriptionId: subscription.id
|
|
992
|
+
});
|
|
993
|
+
return {
|
|
994
|
+
subscriptionId: subscription.id,
|
|
995
|
+
customerId: customer.externalId,
|
|
996
|
+
status: "failed",
|
|
997
|
+
error: "Payment adapter does not support direct charging",
|
|
998
|
+
planChanged: planChanged ? { from: subscription.planCode, to: newPlanCode } : void 0
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
const plan = ctx.internalAdapter.findPlanByCode(newPlanCode);
|
|
1002
|
+
const chargeResult = await ctx.paymentAdapter.charge({
|
|
1003
|
+
customer: {
|
|
1004
|
+
id: customer.id,
|
|
1005
|
+
email: customer.email,
|
|
1006
|
+
providerCustomerId: customer.providerCustomerId
|
|
1007
|
+
},
|
|
1008
|
+
amount,
|
|
1009
|
+
currency: price.currency,
|
|
1010
|
+
description: `Renewal: ${plan?.name ?? newPlanCode} (${newInterval})`,
|
|
1011
|
+
metadata: {
|
|
1012
|
+
subscriptionId: subscription.id,
|
|
1013
|
+
customerId: customer.id,
|
|
1014
|
+
type: "renewal",
|
|
1015
|
+
planCode: newPlanCode
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
if (chargeResult.status === "failed") {
|
|
1019
|
+
ctx.logger.warn("Renewal charge failed", {
|
|
1020
|
+
subscriptionId: subscription.id,
|
|
1021
|
+
error: chargeResult.error
|
|
1022
|
+
});
|
|
1023
|
+
await ctx.internalAdapter.updateSubscription(subscription.id, {
|
|
1024
|
+
status: "past_due"
|
|
1025
|
+
});
|
|
1026
|
+
return {
|
|
1027
|
+
subscriptionId: subscription.id,
|
|
1028
|
+
customerId: customer.externalId,
|
|
1029
|
+
status: "failed",
|
|
1030
|
+
error: chargeResult.error ?? "Charge failed",
|
|
1031
|
+
planChanged: planChanged ? { from: subscription.planCode, to: newPlanCode } : void 0
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
const newPeriodEnd = calculateNewPeriodEnd(now, newInterval);
|
|
1035
|
+
await ctx.internalAdapter.updateSubscription(subscription.id, {
|
|
1036
|
+
currentPeriodStart: now,
|
|
1037
|
+
currentPeriodEnd: newPeriodEnd,
|
|
1038
|
+
status: "active"
|
|
1039
|
+
});
|
|
1040
|
+
await ctx.internalAdapter.createPayment({
|
|
1041
|
+
customerId: customer.id,
|
|
1042
|
+
subscriptionId: subscription.id,
|
|
1043
|
+
type: "renewal",
|
|
1044
|
+
status: "succeeded",
|
|
1045
|
+
amount,
|
|
1046
|
+
currency: price.currency,
|
|
1047
|
+
providerPaymentId: chargeResult.providerPaymentId,
|
|
1048
|
+
metadata: {
|
|
1049
|
+
planCode: newPlanCode,
|
|
1050
|
+
interval: newInterval
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
ctx.logger.info("Renewal succeeded", {
|
|
1054
|
+
subscriptionId: subscription.id,
|
|
1055
|
+
amount,
|
|
1056
|
+
newPeriodEnd: newPeriodEnd.toISOString()
|
|
1057
|
+
});
|
|
1058
|
+
return {
|
|
1059
|
+
subscriptionId: subscription.id,
|
|
1060
|
+
customerId: customer.externalId,
|
|
1061
|
+
status: "succeeded",
|
|
1062
|
+
amount,
|
|
1063
|
+
planChanged: planChanged ? { from: subscription.planCode, to: newPlanCode } : void 0
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
async function processRenewals(ctx, params = {}) {
|
|
1067
|
+
const { dryRun = false } = params;
|
|
1068
|
+
ctx.logger.info("Starting renewal processing", {
|
|
1069
|
+
dryRun,
|
|
1070
|
+
customerId: params.customerId,
|
|
1071
|
+
limit: params.limit
|
|
1072
|
+
});
|
|
1073
|
+
const dueSubscriptions = await findDueSubscriptions(ctx, params);
|
|
1074
|
+
ctx.logger.info(
|
|
1075
|
+
`Found ${dueSubscriptions.length} subscriptions due for renewal`
|
|
1076
|
+
);
|
|
1077
|
+
const result = {
|
|
1078
|
+
processed: 0,
|
|
1079
|
+
succeeded: 0,
|
|
1080
|
+
failed: 0,
|
|
1081
|
+
skipped: 0,
|
|
1082
|
+
renewals: []
|
|
1083
|
+
};
|
|
1084
|
+
for (const subscription of dueSubscriptions) {
|
|
1085
|
+
result.processed++;
|
|
1086
|
+
try {
|
|
1087
|
+
const renewalResult = await processSubscriptionRenewal(
|
|
1088
|
+
ctx,
|
|
1089
|
+
subscription,
|
|
1090
|
+
dryRun
|
|
1091
|
+
);
|
|
1092
|
+
result.renewals.push(renewalResult);
|
|
1093
|
+
if (renewalResult.status === "succeeded") {
|
|
1094
|
+
result.succeeded++;
|
|
1095
|
+
} else if (renewalResult.status === "failed") {
|
|
1096
|
+
result.failed++;
|
|
1097
|
+
} else {
|
|
1098
|
+
result.skipped++;
|
|
1099
|
+
}
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
ctx.logger.error("Error processing renewal", {
|
|
1102
|
+
subscriptionId: subscription.id,
|
|
1103
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1104
|
+
});
|
|
1105
|
+
result.failed++;
|
|
1106
|
+
result.renewals.push({
|
|
1107
|
+
subscriptionId: subscription.id,
|
|
1108
|
+
customerId: subscription.customerId,
|
|
1109
|
+
status: "failed",
|
|
1110
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
ctx.logger.info("Renewal processing complete", {
|
|
1115
|
+
processed: result.processed,
|
|
1116
|
+
succeeded: result.succeeded,
|
|
1117
|
+
failed: result.failed,
|
|
1118
|
+
skipped: result.skipped
|
|
1119
|
+
});
|
|
1120
|
+
return result;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// src/api/routes/renewals.ts
|
|
1124
|
+
var processRenewalsQuerySchema = z.object({
|
|
1125
|
+
/**
|
|
1126
|
+
* Process only a specific customer (useful for testing)
|
|
1127
|
+
*/
|
|
1128
|
+
customerId: z.string().optional(),
|
|
1129
|
+
/**
|
|
1130
|
+
* Dry run - don't actually charge, just report what would happen
|
|
1131
|
+
*/
|
|
1132
|
+
dryRun: z.string().transform((val) => val === "true").optional(),
|
|
1133
|
+
/**
|
|
1134
|
+
* Maximum number of subscriptions to process (for batching)
|
|
1135
|
+
*/
|
|
1136
|
+
limit: z.string().transform((val) => Number.parseInt(val, 10)).refine((val) => !Number.isNaN(val) && val > 0, {
|
|
1137
|
+
message: "limit must be a positive number"
|
|
1138
|
+
}).optional()
|
|
1139
|
+
});
|
|
1140
|
+
var renewalEndpoints = {
|
|
1141
|
+
processRenewals: {
|
|
1142
|
+
path: "/renewals",
|
|
1143
|
+
options: {
|
|
1144
|
+
method: "GET",
|
|
1145
|
+
query: processRenewalsQuerySchema
|
|
1146
|
+
},
|
|
1147
|
+
handler: async (context) => {
|
|
1148
|
+
const { ctx, query } = context;
|
|
1149
|
+
const result = await processRenewals(ctx, {
|
|
1150
|
+
customerId: query.customerId,
|
|
1151
|
+
dryRun: query.dryRun ?? false,
|
|
1152
|
+
limit: query.limit
|
|
1153
|
+
});
|
|
1154
|
+
return result;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
765
1158
|
var getSubscriptionQuerySchema = z.object({
|
|
766
1159
|
customerId: z.string().min(1)
|
|
767
1160
|
});
|
|
@@ -954,6 +1347,7 @@ function getEndpoints(ctx) {
|
|
|
954
1347
|
...featureEndpoints,
|
|
955
1348
|
...paymentEndpoints,
|
|
956
1349
|
...refundEndpoints,
|
|
1350
|
+
...renewalEndpoints,
|
|
957
1351
|
...webhookEndpoints
|
|
958
1352
|
};
|
|
959
1353
|
let allEndpoints = { ...baseEndpoints };
|
|
@@ -1154,7 +1548,7 @@ function featureConfigToFeature(config) {
|
|
|
1154
1548
|
type: config.type ?? "boolean"
|
|
1155
1549
|
};
|
|
1156
1550
|
}
|
|
1157
|
-
function createInternalAdapter(adapter, plans = [], features = []) {
|
|
1551
|
+
function createInternalAdapter(adapter, plans = [], features = [], getNow = async () => /* @__PURE__ */ new Date()) {
|
|
1158
1552
|
const plansByCode = /* @__PURE__ */ new Map();
|
|
1159
1553
|
for (const config of plans) {
|
|
1160
1554
|
plansByCode.set(config.code, planConfigToPlan(config));
|
|
@@ -1166,7 +1560,7 @@ function createInternalAdapter(adapter, plans = [], features = []) {
|
|
|
1166
1560
|
return {
|
|
1167
1561
|
// Customer operations (DB)
|
|
1168
1562
|
async createCustomer(data) {
|
|
1169
|
-
const now =
|
|
1563
|
+
const now = await getNow();
|
|
1170
1564
|
return adapter.create({
|
|
1171
1565
|
model: TABLES.CUSTOMER,
|
|
1172
1566
|
data: {
|
|
@@ -1238,7 +1632,7 @@ function createInternalAdapter(adapter, plans = [], features = []) {
|
|
|
1238
1632
|
},
|
|
1239
1633
|
// Subscription operations (DB)
|
|
1240
1634
|
async createSubscription(data) {
|
|
1241
|
-
const now =
|
|
1635
|
+
const now = await getNow();
|
|
1242
1636
|
const interval = data.interval ?? "monthly";
|
|
1243
1637
|
const currentPeriodEnd = new Date(now);
|
|
1244
1638
|
if (interval === "yearly") {
|
|
@@ -1308,14 +1702,15 @@ function createInternalAdapter(adapter, plans = [], features = []) {
|
|
|
1308
1702
|
});
|
|
1309
1703
|
},
|
|
1310
1704
|
async updateSubscription(id, data) {
|
|
1705
|
+
const now = await getNow();
|
|
1311
1706
|
return adapter.update({
|
|
1312
1707
|
model: TABLES.SUBSCRIPTION,
|
|
1313
1708
|
where: [{ field: "id", operator: "eq", value: id }],
|
|
1314
|
-
update: { ...data, updatedAt:
|
|
1709
|
+
update: { ...data, updatedAt: now }
|
|
1315
1710
|
});
|
|
1316
1711
|
},
|
|
1317
1712
|
async cancelSubscription(id, cancelAt) {
|
|
1318
|
-
const now =
|
|
1713
|
+
const now = await getNow();
|
|
1319
1714
|
return adapter.update({
|
|
1320
1715
|
model: TABLES.SUBSCRIPTION,
|
|
1321
1716
|
where: [{ field: "id", operator: "eq", value: id }],
|
|
@@ -1358,7 +1753,7 @@ function createInternalAdapter(adapter, plans = [], features = []) {
|
|
|
1358
1753
|
},
|
|
1359
1754
|
// Payment operations (DB)
|
|
1360
1755
|
async createPayment(data) {
|
|
1361
|
-
const now =
|
|
1756
|
+
const now = await getNow();
|
|
1362
1757
|
return adapter.create({
|
|
1363
1758
|
model: TABLES.PAYMENT,
|
|
1364
1759
|
data: {
|
|
@@ -1467,10 +1862,12 @@ async function createBillingContext(adapter, options) {
|
|
|
1467
1862
|
schema = { ...schema, ...plugin.schema };
|
|
1468
1863
|
}
|
|
1469
1864
|
}
|
|
1865
|
+
const getNow = async () => /* @__PURE__ */ new Date();
|
|
1470
1866
|
const internalAdapter = createInternalAdapter(
|
|
1471
1867
|
adapter,
|
|
1472
1868
|
options.plans ?? [],
|
|
1473
|
-
options.features ?? []
|
|
1869
|
+
options.features ?? [],
|
|
1870
|
+
getNow
|
|
1474
1871
|
);
|
|
1475
1872
|
const context = {
|
|
1476
1873
|
options: resolvedOptions,
|
|
@@ -1482,6 +1879,7 @@ async function createBillingContext(adapter, options) {
|
|
|
1482
1879
|
plugins,
|
|
1483
1880
|
logger,
|
|
1484
1881
|
secret: resolvedOptions.secret,
|
|
1882
|
+
timeProvider: createDefaultTimeProvider(),
|
|
1485
1883
|
hasPlugin(id) {
|
|
1486
1884
|
return plugins.some((p) => p.id === id);
|
|
1487
1885
|
},
|
|
@@ -1621,6 +2019,10 @@ function createAPI(contextPromise) {
|
|
|
1621
2019
|
amount: params.amount,
|
|
1622
2020
|
reason: params.reason
|
|
1623
2021
|
});
|
|
2022
|
+
},
|
|
2023
|
+
async processRenewals(params) {
|
|
2024
|
+
const ctx = await contextPromise;
|
|
2025
|
+
return processRenewals(ctx, params);
|
|
1624
2026
|
}
|
|
1625
2027
|
};
|
|
1626
2028
|
}
|