@nextsparkjs/core 0.1.0-beta.127 → 0.1.0-beta.128
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/components/billing/ManageBillingButton.d.ts +3 -3
- package/dist/lib/api/rate-limit.d.ts.map +1 -1
- package/dist/lib/api/rate-limit.js +9 -6
- package/dist/lib/billing/config-types.d.ts +2 -5
- package/dist/lib/billing/config-types.d.ts.map +1 -1
- package/dist/lib/billing/gateways/factory.d.ts +13 -2
- package/dist/lib/billing/gateways/factory.d.ts.map +1 -1
- package/dist/lib/billing/gateways/factory.js +13 -6
- package/dist/lib/billing/gateways/interface.d.ts +19 -1
- package/dist/lib/billing/gateways/interface.d.ts.map +1 -1
- package/dist/lib/billing/gateways/polar.d.ts +8 -1
- package/dist/lib/billing/gateways/polar.d.ts.map +1 -1
- package/dist/lib/billing/gateways/polar.js +25 -0
- package/dist/lib/billing/gateways/stripe.d.ts +8 -26
- package/dist/lib/billing/gateways/stripe.d.ts.map +1 -1
- package/dist/lib/billing/gateways/stripe.js +41 -44
- package/dist/lib/billing/gateways/types.d.ts +11 -0
- package/dist/lib/billing/gateways/types.d.ts.map +1 -1
- package/dist/lib/billing/jobs.d.ts +1 -1
- package/dist/lib/billing/polar-webhook.d.ts +38 -0
- package/dist/lib/billing/polar-webhook.d.ts.map +1 -0
- package/dist/lib/billing/polar-webhook.js +0 -0
- package/dist/lib/billing/schema.d.ts +1 -2
- package/dist/lib/billing/schema.d.ts.map +1 -1
- package/dist/lib/billing/schema.js +1 -1
- package/dist/lib/billing/stripe-webhook.d.ts +48 -0
- package/dist/lib/billing/stripe-webhook.d.ts.map +1 -0
- package/dist/lib/billing/stripe-webhook.js +316 -0
- package/dist/lib/billing/types.d.ts +6 -2
- package/dist/lib/billing/types.d.ts.map +1 -1
- package/dist/lib/rate-limit-redis.d.ts +2 -2
- package/dist/lib/rate-limit-redis.d.ts.map +1 -1
- package/dist/lib/rate-limit-redis.js +22 -4
- package/dist/lib/selectors/core-selectors.d.ts +2 -2
- package/dist/lib/selectors/domains/superadmin.selectors.d.ts +2 -2
- package/dist/lib/selectors/domains/superadmin.selectors.js +2 -2
- package/dist/lib/selectors/selectors.d.ts +4 -4
- package/dist/lib/services/invoice.service.d.ts +3 -3
- package/dist/lib/services/invoice.service.js +2 -2
- package/dist/lib/services/membership.service.d.ts.map +1 -1
- package/dist/lib/services/membership.service.js +29 -0
- package/dist/lib/services/plan.service.d.ts +0 -3
- package/dist/lib/services/plan.service.d.ts.map +1 -1
- package/dist/lib/services/plan.service.js +3 -9
- package/dist/lib/services/subscription.service.d.ts +5 -5
- package/dist/lib/services/subscription.service.d.ts.map +1 -1
- package/dist/lib/services/subscription.service.js +54 -41
- package/dist/migrations/001_better_auth_and_functions.sql +5 -11
- package/dist/migrations/008_team_members_table.sql +27 -23
- package/dist/styles/classes.json +1 -1
- package/dist/templates/app/api/auth/[...all]/route.ts +35 -0
- package/dist/templates/app/api/health/route.ts +43 -23
- package/dist/templates/app/api/internal/user-metadata/route.ts +10 -0
- package/dist/templates/app/api/superadmin/subscriptions/route.ts +5 -0
- package/dist/templates/app/api/superadmin/teams/[teamId]/route.ts +6 -0
- package/dist/templates/app/api/v1/billing/cancel/route.ts +8 -10
- package/dist/templates/app/api/v1/billing/change-plan/route.ts +2 -2
- package/dist/templates/app/api/v1/billing/checkout/route.ts +6 -8
- package/dist/templates/app/api/v1/billing/portal/route.ts +5 -5
- package/dist/templates/app/api/v1/billing/presets.ts +1 -1
- package/dist/templates/app/api/v1/billing/webhooks/polar/route.ts +83 -6
- package/dist/templates/app/api/v1/billing/webhooks/stripe/route.ts +18 -421
- package/dist/templates/app/layout.tsx +14 -5
- package/dist/templates/app/superadmin/subscriptions/page.tsx +16 -14
- package/dist/templates/app/superadmin/teams/[teamId]/page.tsx +18 -15
- package/dist/templates/contents/themes/starter/tests/cypress/src/features/SuperadminPOM.ts +2 -2
- package/dist/templates/lib/billing/polar-webhook-extensions.ts +23 -0
- package/dist/templates/lib/billing/stripe-webhook-extensions.ts +23 -0
- package/migrations/001_better_auth_and_functions.sql +5 -11
- package/migrations/008_team_members_table.sql +27 -23
- package/package.json +10 -2
- package/scripts/build/registry/generators/billing-registry.mjs +1 -2
- package/templates/app/api/auth/[...all]/route.ts +35 -0
- package/templates/app/api/health/route.ts +43 -23
- package/templates/app/api/internal/user-metadata/route.ts +10 -0
- package/templates/app/api/superadmin/subscriptions/route.ts +5 -0
- package/templates/app/api/superadmin/teams/[teamId]/route.ts +6 -0
- package/templates/app/api/v1/billing/cancel/route.ts +8 -10
- package/templates/app/api/v1/billing/change-plan/route.ts +2 -2
- package/templates/app/api/v1/billing/checkout/route.ts +6 -8
- package/templates/app/api/v1/billing/portal/route.ts +5 -5
- package/templates/app/api/v1/billing/presets.ts +1 -1
- package/templates/app/api/v1/billing/webhooks/polar/route.ts +83 -6
- package/templates/app/api/v1/billing/webhooks/stripe/route.ts +18 -421
- package/templates/app/layout.tsx +14 -5
- package/templates/app/superadmin/subscriptions/page.tsx +16 -14
- package/templates/app/superadmin/teams/[teamId]/page.tsx +18 -15
- package/templates/contents/themes/starter/tests/cypress/src/features/SuperadminPOM.ts +2 -2
- package/templates/lib/billing/polar-webhook-extensions.ts +23 -0
- package/templates/lib/billing/stripe-webhook-extensions.ts +23 -0
- package/tests/jest/__mocks__/@nextsparkjs/registries/billing-registry.ts +7 -8
|
@@ -3,6 +3,7 @@ import { TeamMemberService } from "./team-member.service.js";
|
|
|
3
3
|
import { SubscriptionService } from "./subscription.service.js";
|
|
4
4
|
import { PermissionService } from "./permission.service.js";
|
|
5
5
|
import { PlanService } from "./plan.service.js";
|
|
6
|
+
import { BILLING_REGISTRY } from "@nextsparkjs/registries/billing-registry";
|
|
6
7
|
class TeamMembership {
|
|
7
8
|
userId;
|
|
8
9
|
teamId;
|
|
@@ -141,6 +142,7 @@ class TeamMembership {
|
|
|
141
142
|
* }
|
|
142
143
|
*/
|
|
143
144
|
canPerformAction(action, options) {
|
|
145
|
+
var _a, _b, _c, _d, _e;
|
|
144
146
|
if (!this.role) {
|
|
145
147
|
return {
|
|
146
148
|
allowed: false,
|
|
@@ -173,6 +175,33 @@ class TeamMembership {
|
|
|
173
175
|
};
|
|
174
176
|
}
|
|
175
177
|
}
|
|
178
|
+
const requiredFeature = (_b = (_a = BILLING_REGISTRY.actionMappings) == null ? void 0 : _a.features) == null ? void 0 : _b[action];
|
|
179
|
+
if (requiredFeature && !this.hasFeature(requiredFeature)) {
|
|
180
|
+
return {
|
|
181
|
+
allowed: false,
|
|
182
|
+
reason: "feature_not_in_plan",
|
|
183
|
+
message: `Your plan does not include the required feature: ${requiredFeature}`,
|
|
184
|
+
meta: {
|
|
185
|
+
requiredFeature,
|
|
186
|
+
currentPlan: (_c = this.subscription) == null ? void 0 : _c.planSlug
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const consumedLimit = (_e = (_d = BILLING_REGISTRY.actionMappings) == null ? void 0 : _d.limits) == null ? void 0 : _e[action];
|
|
191
|
+
if (consumedLimit) {
|
|
192
|
+
const quotaCheck = this.checkQuota(consumedLimit, (options == null ? void 0 : options.incrementQuota) ?? 1);
|
|
193
|
+
if (!quotaCheck.allowed) {
|
|
194
|
+
return {
|
|
195
|
+
allowed: false,
|
|
196
|
+
reason: "quota_exceeded",
|
|
197
|
+
message: `Quota exceeded for ${consumedLimit}`,
|
|
198
|
+
meta: {
|
|
199
|
+
limitSlug: consumedLimit,
|
|
200
|
+
remaining: quotaCheck.remaining
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
176
205
|
return { allowed: true };
|
|
177
206
|
}
|
|
178
207
|
}
|
|
@@ -161,7 +161,6 @@ export declare class PlanService {
|
|
|
161
161
|
static getPrice(slug: string, interval: 'monthly' | 'yearly'): number | null;
|
|
162
162
|
/**
|
|
163
163
|
* Get provider price ID for a plan.
|
|
164
|
-
* Checks generic providerPriceIds first, falls back to Stripe-specific fields.
|
|
165
164
|
*
|
|
166
165
|
* @param slug - Plan slug
|
|
167
166
|
* @param interval - 'monthly' or 'yearly'
|
|
@@ -171,7 +170,5 @@ export declare class PlanService {
|
|
|
171
170
|
* const priceId = PlanService.getPriceId('pro', 'monthly')
|
|
172
171
|
*/
|
|
173
172
|
static getPriceId(slug: string, interval: 'monthly' | 'yearly'): string | null;
|
|
174
|
-
/** @deprecated Use getPriceId instead */
|
|
175
|
-
static getStripePriceId(slug: string, interval: 'monthly' | 'yearly'): string | null;
|
|
176
173
|
}
|
|
177
174
|
//# sourceMappingURL=plan.service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan.service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/plan.service.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAM7D,MAAM,WAAW,gBAAgB;IAC/B,4CAA4C;IAC5C,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAMD,qBAAa,WAAW;IAKtB;;;;;;;;OAQG;WACU,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAatD;;;;;;;;OAQG;WACU,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAa1D;;;;;;;;;;;;OAYG;WACU,IAAI,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAclE;;;;;;;;OAQG;WACU,UAAU,IAAI,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAK/C;;;;;;;;OAQG;WACU,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBnD;;;;;;;;;;OAUG;IACH,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IASrD;;;;;;;;;OASG;IACH,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE;IAc1C;;;;;;;;;OASG;IACH,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAStD;;;;;;;;;;OAUG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM;IAKxD;;;;;;;;;OASG;IACH,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO;IAS7D;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO;IAc3D;;;;;;;;OAQG;IACH,MAAM,CAAC,WAAW,IAAI,MAAM,EAAE;IAI9B;;;;;;;;;;OAUG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,IAAI;IAS5E
|
|
1
|
+
{"version":3,"file":"plan.service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/plan.service.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAM7D,MAAM,WAAW,gBAAgB;IAC/B,4CAA4C;IAC5C,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAMD,qBAAa,WAAW;IAKtB;;;;;;;;OAQG;WACU,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAatD;;;;;;;;OAQG;WACU,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAa1D;;;;;;;;;;;;OAYG;WACU,IAAI,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAclE;;;;;;;;OAQG;WACU,UAAU,IAAI,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAK/C;;;;;;;;OAQG;WACU,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBnD;;;;;;;;;;OAUG;IACH,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IASrD;;;;;;;;;OASG;IACH,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE;IAc1C;;;;;;;;;OASG;IACH,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAStD;;;;;;;;;;OAUG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM;IAKxD;;;;;;;;;OASG;IACH,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO;IAS7D;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO;IAc3D;;;;;;;;OAQG;IACH,MAAM,CAAC,WAAW,IAAI,MAAM,EAAE;IAI9B;;;;;;;;;;OAUG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,IAAI;IAS5E;;;;;;;;;OASG;IACH,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,IAAI;CAc/E"}
|
|
@@ -243,7 +243,6 @@ class PlanService {
|
|
|
243
243
|
}
|
|
244
244
|
/**
|
|
245
245
|
* Get provider price ID for a plan.
|
|
246
|
-
* Checks generic providerPriceIds first, falls back to Stripe-specific fields.
|
|
247
246
|
*
|
|
248
247
|
* @param slug - Plan slug
|
|
249
248
|
* @param interval - 'monthly' or 'yearly'
|
|
@@ -257,15 +256,10 @@ class PlanService {
|
|
|
257
256
|
if (!config) {
|
|
258
257
|
return null;
|
|
259
258
|
}
|
|
260
|
-
if (config.providerPriceIds) {
|
|
261
|
-
|
|
262
|
-
if (id) return id;
|
|
259
|
+
if (!config.providerPriceIds) {
|
|
260
|
+
return null;
|
|
263
261
|
}
|
|
264
|
-
return interval === "yearly" ? config.
|
|
265
|
-
}
|
|
266
|
-
/** @deprecated Use getPriceId instead */
|
|
267
|
-
static getStripePriceId(slug, interval) {
|
|
268
|
-
return this.getPriceId(slug, interval);
|
|
262
|
+
return interval === "yearly" ? config.providerPriceIds.yearly || null : config.providerPriceIds.monthly || null;
|
|
269
263
|
}
|
|
270
264
|
}
|
|
271
265
|
export {
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module SubscriptionService
|
|
8
8
|
*/
|
|
9
|
-
import type { Subscription, SubscriptionWithPlan, SubscriptionStatus, QuotaInfo, CanPerformActionResult, BillingInterval } from '../billing/types';
|
|
9
|
+
import type { Subscription, SubscriptionWithPlan, SubscriptionStatus, QuotaInfo, CanPerformActionResult, BillingInterval, PaymentProvider } from '../billing/types';
|
|
10
10
|
export interface CreateSubscriptionOptions {
|
|
11
11
|
billingInterval?: BillingInterval;
|
|
12
12
|
trialDays?: number;
|
|
13
|
-
paymentProvider?:
|
|
13
|
+
paymentProvider?: PaymentProvider;
|
|
14
14
|
externalSubscriptionId?: string;
|
|
15
15
|
externalCustomerId?: string;
|
|
16
16
|
}
|
|
@@ -41,7 +41,7 @@ export declare class SubscriptionService {
|
|
|
41
41
|
* const sub = await SubscriptionService.getActive('team-uuid-123')
|
|
42
42
|
* console.log(sub?.plan.name) // 'Pro Plan'
|
|
43
43
|
*/
|
|
44
|
-
static getActive(teamId: string): Promise<SubscriptionWithPlan | null>;
|
|
44
|
+
static getActive(teamId: string, userId?: string): Promise<SubscriptionWithPlan | null>;
|
|
45
45
|
/**
|
|
46
46
|
* Get subscription by team ID (alias for getActive)
|
|
47
47
|
*
|
|
@@ -117,7 +117,7 @@ export declare class SubscriptionService {
|
|
|
117
117
|
* console.log('Warnings:', result.downgradeWarnings)
|
|
118
118
|
* }
|
|
119
119
|
*/
|
|
120
|
-
static changePlan(teamId: string, targetPlanSlug: string, billingInterval?: BillingInterval): Promise<ChangePlanResult>;
|
|
120
|
+
static changePlan(teamId: string, targetPlanSlug: string, billingInterval?: BillingInterval, userId?: string): Promise<ChangePlanResult>;
|
|
121
121
|
/**
|
|
122
122
|
* Pause subscription
|
|
123
123
|
*
|
|
@@ -180,7 +180,7 @@ export declare class SubscriptionService {
|
|
|
180
180
|
offset?: number;
|
|
181
181
|
}): Promise<SubscriptionWithPlan[]>;
|
|
182
182
|
/**
|
|
183
|
-
* Get subscription by external (
|
|
183
|
+
* Get subscription by external subscription ID (from payment provider)
|
|
184
184
|
*
|
|
185
185
|
* @param externalId - External subscription ID from payment provider
|
|
186
186
|
* @returns Subscription with plan or null
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscription.service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/subscription.service.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAcH,OAAO,KAAK,EAEV,YAAY,EACZ,oBAAoB,EACpB,kBAAkB,EAClB,SAAS,EAET,sBAAsB,EACtB,eAAe,EAChB,MAAM,kBAAkB,CAAA;AAMzB,MAAM,WAAW,yBAAyB;IACxC,eAAe,CAAC,EAAE,eAAe,CAAA;IACjC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,eAAe,CAAC,EAAE,
|
|
1
|
+
{"version":3,"file":"subscription.service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/subscription.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { queryOneWithRLS, queryWithRLS, mutateWithRLS } from "../db.js";
|
|
1
|
+
import { queryOneWithRLS, queryWithRLS, mutateWithRLS, getTransactionClient } from "../db.js";
|
|
2
2
|
import { doAction } from "../plugins/hook-system.js";
|
|
3
3
|
import { BILLING_REGISTRY } from "@nextsparkjs/registries/billing-registry";
|
|
4
4
|
import { PlanService } from "./plan.service.js";
|
|
@@ -41,7 +41,7 @@ class SubscriptionService {
|
|
|
41
41
|
* const sub = await SubscriptionService.getActive('team-uuid-123')
|
|
42
42
|
* console.log(sub?.plan.name) // 'Pro Plan'
|
|
43
43
|
*/
|
|
44
|
-
static async getActive(teamId) {
|
|
44
|
+
static async getActive(teamId, userId) {
|
|
45
45
|
if (!teamId || teamId.trim() === "") {
|
|
46
46
|
throw new Error("Team ID is required");
|
|
47
47
|
}
|
|
@@ -57,7 +57,8 @@ class SubscriptionService {
|
|
|
57
57
|
ORDER BY s."createdAt" DESC
|
|
58
58
|
LIMIT 1
|
|
59
59
|
`,
|
|
60
|
-
[teamId]
|
|
60
|
+
[teamId],
|
|
61
|
+
userId
|
|
61
62
|
);
|
|
62
63
|
if (!result) return null;
|
|
63
64
|
return {
|
|
@@ -325,14 +326,14 @@ class SubscriptionService {
|
|
|
325
326
|
* console.log('Warnings:', result.downgradeWarnings)
|
|
326
327
|
* }
|
|
327
328
|
*/
|
|
328
|
-
static async changePlan(teamId, targetPlanSlug, billingInterval = "monthly") {
|
|
329
|
+
static async changePlan(teamId, targetPlanSlug, billingInterval = "monthly", userId) {
|
|
329
330
|
if (!teamId || teamId.trim() === "") {
|
|
330
331
|
return { success: false, error: "Team ID is required" };
|
|
331
332
|
}
|
|
332
333
|
if (!targetPlanSlug || targetPlanSlug.trim() === "") {
|
|
333
334
|
return { success: false, error: "Target plan slug is required" };
|
|
334
335
|
}
|
|
335
|
-
const currentSub = await this.getActive(teamId);
|
|
336
|
+
const currentSub = await this.getActive(teamId, userId);
|
|
336
337
|
if (!(currentSub == null ? void 0 : currentSub.externalSubscriptionId)) {
|
|
337
338
|
return { success: false, error: "No active subscription found" };
|
|
338
339
|
}
|
|
@@ -354,46 +355,58 @@ class SubscriptionService {
|
|
|
354
355
|
try {
|
|
355
356
|
await getBillingGateway().updateSubscriptionPlan({
|
|
356
357
|
subscriptionId: currentSub.externalSubscriptionId,
|
|
357
|
-
newPriceId
|
|
358
|
-
prorationBehavior: "create_prorations"
|
|
358
|
+
newPriceId
|
|
359
359
|
});
|
|
360
360
|
const targetPlan = await PlanService.getBySlug(targetPlanSlug);
|
|
361
361
|
if (!targetPlan) {
|
|
362
362
|
return { success: false, error: "Plan not found in database" };
|
|
363
363
|
}
|
|
364
|
-
await
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
"
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
364
|
+
const tx = await getTransactionClient();
|
|
365
|
+
try {
|
|
366
|
+
await tx.query(
|
|
367
|
+
`
|
|
368
|
+
UPDATE "subscriptions"
|
|
369
|
+
SET
|
|
370
|
+
"planId" = $1,
|
|
371
|
+
"billingInterval" = $2,
|
|
372
|
+
metadata = jsonb_set(
|
|
373
|
+
COALESCE(metadata, '{}'::jsonb),
|
|
374
|
+
'{previousPlanSlug}',
|
|
375
|
+
$3::jsonb
|
|
376
|
+
),
|
|
377
|
+
"updatedAt" = NOW()
|
|
378
|
+
WHERE "teamId" = $4 AND status IN ('active', 'trialing')
|
|
379
|
+
`,
|
|
380
|
+
[targetPlan.id, billingInterval, JSON.stringify(currentSub.plan.slug), teamId]
|
|
381
|
+
);
|
|
382
|
+
await tx.query(
|
|
383
|
+
`
|
|
384
|
+
INSERT INTO "billing_events" (id, "subscriptionId", type, status, amount, currency, metadata, "createdAt")
|
|
385
|
+
VALUES ($1, $2, 'lifecycle', 'succeeded', $3, $4, $5, NOW())
|
|
386
|
+
`,
|
|
387
|
+
[
|
|
388
|
+
crypto.randomUUID(),
|
|
389
|
+
currentSub.id,
|
|
390
|
+
0,
|
|
391
|
+
"usd",
|
|
392
|
+
JSON.stringify({
|
|
393
|
+
action: "plan_change",
|
|
394
|
+
fromPlan: currentSub.plan.slug,
|
|
395
|
+
toPlan: targetPlanSlug,
|
|
396
|
+
billingInterval
|
|
397
|
+
})
|
|
398
|
+
]
|
|
399
|
+
);
|
|
400
|
+
await tx.commit();
|
|
401
|
+
} catch (dbError) {
|
|
402
|
+
await tx.rollback();
|
|
403
|
+
console.error("[SubscriptionService.changePlan] DB transaction failed after provider update:", dbError);
|
|
404
|
+
return {
|
|
405
|
+
success: false,
|
|
406
|
+
error: "Plan changed in payment provider but local DB update failed. Please contact support."
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
const updatedSub = await this.getActive(teamId, userId);
|
|
397
410
|
await doAction("subscription.updated", {
|
|
398
411
|
id: currentSub.id,
|
|
399
412
|
data: updatedSub,
|
|
@@ -544,7 +557,7 @@ class SubscriptionService {
|
|
|
544
557
|
return results.map((r) => ({ ...r, plan: r.plan }));
|
|
545
558
|
}
|
|
546
559
|
/**
|
|
547
|
-
* Get subscription by external (
|
|
560
|
+
* Get subscription by external subscription ID (from payment provider)
|
|
548
561
|
*
|
|
549
562
|
* @param externalId - External subscription ID from payment provider
|
|
550
563
|
* @returns Subscription with plan or null
|
|
@@ -41,17 +41,11 @@ BEGIN
|
|
|
41
41
|
END;
|
|
42
42
|
$$;
|
|
43
43
|
|
|
44
|
-
--
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
SET search_path = public
|
|
50
|
-
AS $$
|
|
51
|
-
BEGIN
|
|
52
|
-
RETURN public.get_auth_user_id();
|
|
53
|
-
END;
|
|
54
|
-
$$;
|
|
44
|
+
-- NOTE: Previously there was an alias function `get_auth_user_id()` (without schema)
|
|
45
|
+
-- that called `public.get_auth_user_id()`. This was removed because PostgreSQL's
|
|
46
|
+
-- `CREATE OR REPLACE` with `SET search_path = public` would overwrite the real
|
|
47
|
+
-- function with the alias, causing infinite recursion. All RLS policies should
|
|
48
|
+
-- use `public.get_auth_user_id()` with the explicit schema qualifier.
|
|
55
49
|
|
|
56
50
|
-- Utilidad: updatedAt (si no existe ya en otra migration)
|
|
57
51
|
CREATE OR REPLACE FUNCTION public.set_updated_at()
|
|
@@ -43,6 +43,24 @@ CREATE TRIGGER team_members_set_updated_at
|
|
|
43
43
|
BEFORE UPDATE ON public."team_members"
|
|
44
44
|
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
|
45
45
|
|
|
46
|
+
-- ============================================
|
|
47
|
+
-- HELPER FUNCTION (bypasses RLS via SECURITY DEFINER)
|
|
48
|
+
-- ============================================
|
|
49
|
+
|
|
50
|
+
-- Returns team IDs for a given user, bypassing RLS to prevent self-referencing
|
|
51
|
+
-- recursion. Without this, team_members RLS policies query team_members itself,
|
|
52
|
+
-- causing infinite recursion when any other table (subscriptions, billing_events,
|
|
53
|
+
-- usage) references team_members in its own RLS policy chain.
|
|
54
|
+
CREATE OR REPLACE FUNCTION public.get_user_team_ids(p_user_id TEXT)
|
|
55
|
+
RETURNS SETOF TEXT
|
|
56
|
+
LANGUAGE sql
|
|
57
|
+
STABLE
|
|
58
|
+
SECURITY DEFINER
|
|
59
|
+
SET search_path = public
|
|
60
|
+
AS $$
|
|
61
|
+
SELECT "teamId" FROM public."team_members" WHERE "userId" = p_user_id;
|
|
62
|
+
$$;
|
|
63
|
+
|
|
46
64
|
-- ============================================
|
|
47
65
|
-- TEAM MEMBERS ROW LEVEL SECURITY (RLS)
|
|
48
66
|
-- ============================================
|
|
@@ -50,23 +68,21 @@ FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
|
|
50
68
|
ALTER TABLE public."team_members" ENABLE ROW LEVEL SECURITY;
|
|
51
69
|
|
|
52
70
|
-- Team members: visible to members of the same team
|
|
71
|
+
-- Uses get_user_team_ids() (SECURITY DEFINER) to avoid self-referencing recursion
|
|
53
72
|
CREATE POLICY "team_members_select_policy" ON public."team_members"
|
|
54
73
|
FOR SELECT TO authenticated
|
|
55
74
|
USING (
|
|
56
|
-
"teamId" IN (
|
|
57
|
-
SELECT "teamId" FROM public."team_members"
|
|
58
|
-
WHERE "userId" = public.get_auth_user_id()
|
|
59
|
-
)
|
|
75
|
+
"teamId" IN (SELECT public.get_user_team_ids(public.get_auth_user_id()))
|
|
60
76
|
);
|
|
61
77
|
|
|
62
78
|
-- Team members: owners and admins can add members
|
|
63
79
|
CREATE POLICY "team_members_insert_policy" ON public."team_members"
|
|
64
80
|
FOR INSERT TO authenticated
|
|
65
81
|
WITH CHECK (
|
|
66
|
-
"teamId" IN (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
82
|
+
"teamId" IN (SELECT public.get_user_team_ids(public.get_auth_user_id()))
|
|
83
|
+
AND EXISTS (
|
|
84
|
+
SELECT 1 FROM public.get_user_team_ids(public.get_auth_user_id()) AS t
|
|
85
|
+
WHERE t = "team_members"."teamId"
|
|
70
86
|
)
|
|
71
87
|
);
|
|
72
88
|
|
|
@@ -74,29 +90,17 @@ CREATE POLICY "team_members_insert_policy" ON public."team_members"
|
|
|
74
90
|
CREATE POLICY "team_members_update_policy" ON public."team_members"
|
|
75
91
|
FOR UPDATE TO authenticated
|
|
76
92
|
USING (
|
|
77
|
-
"teamId" IN (
|
|
78
|
-
SELECT "teamId" FROM public."team_members"
|
|
79
|
-
WHERE "userId" = public.get_auth_user_id()
|
|
80
|
-
AND role IN ('owner', 'admin')
|
|
81
|
-
)
|
|
93
|
+
"teamId" IN (SELECT public.get_user_team_ids(public.get_auth_user_id()))
|
|
82
94
|
)
|
|
83
95
|
WITH CHECK (
|
|
84
|
-
"teamId" IN (
|
|
85
|
-
SELECT "teamId" FROM public."team_members"
|
|
86
|
-
WHERE "userId" = public.get_auth_user_id()
|
|
87
|
-
AND role IN ('owner', 'admin')
|
|
88
|
-
)
|
|
96
|
+
"teamId" IN (SELECT public.get_user_team_ids(public.get_auth_user_id()))
|
|
89
97
|
);
|
|
90
98
|
|
|
91
99
|
-- Team members: owners and admins can remove members (except themselves)
|
|
92
100
|
CREATE POLICY "team_members_delete_policy" ON public."team_members"
|
|
93
101
|
FOR DELETE TO authenticated
|
|
94
102
|
USING (
|
|
95
|
-
"teamId" IN (
|
|
96
|
-
SELECT "teamId" FROM public."team_members"
|
|
97
|
-
WHERE "userId" = public.get_auth_user_id()
|
|
98
|
-
AND role IN ('owner', 'admin')
|
|
99
|
-
)
|
|
103
|
+
"teamId" IN (SELECT public.get_user_team_ids(public.get_auth_user_id()))
|
|
100
104
|
-- Cannot remove yourself
|
|
101
105
|
AND "userId" != public.get_auth_user_id()
|
|
102
106
|
);
|
package/dist/styles/classes.json
CHANGED
|
@@ -7,6 +7,7 @@ import { isPublicSignupRestricted } from "@nextsparkjs/core/lib/teams/helpers";
|
|
|
7
7
|
// Currently domain validation happens in auth.ts databaseHooks
|
|
8
8
|
import { TeamService } from "@nextsparkjs/core/lib/services";
|
|
9
9
|
import { wrapAuthHandlerWithCors, handleCorsPreflightRequest, addCorsHeaders } from "@nextsparkjs/core/lib/api/helpers";
|
|
10
|
+
import { checkDistributedRateLimit } from "@nextsparkjs/core/lib/api/rate-limit";
|
|
10
11
|
|
|
11
12
|
const handlers = toNextJsHandler(auth);
|
|
12
13
|
|
|
@@ -45,6 +46,40 @@ export async function GET(req: NextRequest, context: { params: Promise<{ all: st
|
|
|
45
46
|
|
|
46
47
|
// Intercept signup requests to validate registration mode
|
|
47
48
|
export async function POST(req: NextRequest) {
|
|
49
|
+
// Rate limiting: 5 requests per 15 minutes per IP (tier: auth).
|
|
50
|
+
// Protects login/signup against brute-force and credential stuffing attacks.
|
|
51
|
+
// IP extraction strategy:
|
|
52
|
+
// - Cloudflare: cf-connecting-ip (set by Cloudflare, not spoofable behind CF)
|
|
53
|
+
// - Vercel/trusted proxies: rightmost non-private IP in x-forwarded-for
|
|
54
|
+
// - Fallback: x-real-ip or 'unknown'
|
|
55
|
+
const clientIp = (() => {
|
|
56
|
+
// Cloudflare sets this header and it cannot be spoofed when behind CF
|
|
57
|
+
const cfIp = req.headers.get('cf-connecting-ip')
|
|
58
|
+
if (cfIp) return cfIp
|
|
59
|
+
|
|
60
|
+
// x-forwarded-for: use rightmost entry (last proxy-appended value is most trustworthy)
|
|
61
|
+
const forwardedFor = req.headers.get('x-forwarded-for')
|
|
62
|
+
if (forwardedFor) {
|
|
63
|
+
const ips = forwardedFor.split(',').map(ip => ip.trim()).filter(Boolean)
|
|
64
|
+
if (ips.length > 0) return ips[ips.length - 1]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return req.headers.get('x-real-ip') || 'unknown'
|
|
68
|
+
})()
|
|
69
|
+
const rateLimitResult = await checkDistributedRateLimit(`auth:ip:${clientIp}`, 'auth')
|
|
70
|
+
if (!rateLimitResult.allowed) {
|
|
71
|
+
return new NextResponse(JSON.stringify({ error: 'Too many requests' }), {
|
|
72
|
+
status: 429,
|
|
73
|
+
headers: {
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
'Retry-After': '900',
|
|
76
|
+
'X-RateLimit-Limit': rateLimitResult.limit.toString(),
|
|
77
|
+
'X-RateLimit-Remaining': '0',
|
|
78
|
+
'X-RateLimit-Reset': rateLimitResult.resetTime.toString(),
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
48
83
|
const pathname = req.nextUrl.pathname;
|
|
49
84
|
|
|
50
85
|
// Determine request type
|
|
@@ -1,31 +1,51 @@
|
|
|
1
1
|
import { queryWithRLS } from "@nextsparkjs/core/lib/db";
|
|
2
2
|
import { withRateLimitTier } from "@nextsparkjs/core/lib/api/rate-limit";
|
|
3
|
-
import {
|
|
3
|
+
import { isRedisConfigured } from "@nextsparkjs/core/lib/rate-limit-redis";
|
|
4
|
+
import { authenticateRequest } from "@nextsparkjs/core/lib/api/auth/dual-auth";
|
|
5
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
6
|
+
|
|
7
|
+
export const GET = withRateLimitTier(async (request: NextRequest) => {
|
|
8
|
+
let dbStatus: 'connected' | 'disconnected' = 'disconnected';
|
|
9
|
+
let dbError: string | undefined;
|
|
4
10
|
|
|
5
|
-
export const GET = withRateLimitTier(async () => {
|
|
6
11
|
try {
|
|
7
|
-
// Test database connection
|
|
8
12
|
await queryWithRLS('SELECT 1');
|
|
9
|
-
|
|
10
|
-
return NextResponse.json({
|
|
11
|
-
status: 'healthy',
|
|
12
|
-
timestamp: new Date().toISOString(),
|
|
13
|
-
services: {
|
|
14
|
-
database: 'connected',
|
|
15
|
-
api: 'operational'
|
|
16
|
-
}
|
|
17
|
-
});
|
|
13
|
+
dbStatus = 'connected';
|
|
18
14
|
} catch (error) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
status: 'unhealthy',
|
|
22
|
-
timestamp: new Date().toISOString(),
|
|
23
|
-
error: 'Database connection failed',
|
|
24
|
-
services: {
|
|
25
|
-
database: 'disconnected',
|
|
26
|
-
api: 'operational'
|
|
27
|
-
}
|
|
28
|
-
}, { status: 503 });
|
|
15
|
+
dbError = error instanceof Error ? error.message : 'Unknown error';
|
|
16
|
+
console.error('[health] Database connection failed:', error);
|
|
29
17
|
}
|
|
30
|
-
}, 'read');
|
|
31
18
|
|
|
19
|
+
const healthy = dbStatus === 'connected';
|
|
20
|
+
|
|
21
|
+
// Public response: only healthy/unhealthy status
|
|
22
|
+
const publicResponse = {
|
|
23
|
+
status: healthy ? 'healthy' : 'unhealthy',
|
|
24
|
+
timestamp: new Date().toISOString(),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Check if caller is authenticated for detailed info
|
|
28
|
+
const authResult = await authenticateRequest(request);
|
|
29
|
+
const isAuthenticated = authResult.success && authResult.user;
|
|
30
|
+
|
|
31
|
+
if (!isAuthenticated) {
|
|
32
|
+
return NextResponse.json(publicResponse, { status: healthy ? 200 : 503 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Authenticated callers get detailed service status
|
|
36
|
+
const redisConfigured = await isRedisConfigured();
|
|
37
|
+
|
|
38
|
+
if (!redisConfigured) {
|
|
39
|
+
console.error('[health] CRITICAL: Redis not configured — rate limiting is DISABLED. Set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN in environment variables.');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return NextResponse.json({
|
|
43
|
+
...publicResponse,
|
|
44
|
+
services: {
|
|
45
|
+
database: dbStatus,
|
|
46
|
+
rateLimit: redisConfigured ? 'redis' : 'disabled',
|
|
47
|
+
api: 'operational',
|
|
48
|
+
},
|
|
49
|
+
...(dbError ? { error: dbError } : {}),
|
|
50
|
+
}, { status: healthy ? 200 : 503 });
|
|
51
|
+
}, 'read');
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server'
|
|
2
2
|
import { MetaService } from '@nextsparkjs/core/lib/services/meta.service'
|
|
3
3
|
import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit'
|
|
4
|
+
import { authenticateRequest } from '@nextsparkjs/core/lib/api/auth/dual-auth'
|
|
4
5
|
|
|
5
6
|
// Endpoint interno para crear metadata default después del signup
|
|
6
7
|
export const POST = withRateLimitTier(async (req: NextRequest) => {
|
|
7
8
|
try {
|
|
9
|
+
const authResult = await authenticateRequest(req)
|
|
10
|
+
if (!authResult.success || !authResult.user) {
|
|
11
|
+
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
|
|
12
|
+
}
|
|
13
|
+
|
|
8
14
|
const body = await req.json()
|
|
9
15
|
const { userId, metadata } = body
|
|
10
16
|
|
|
@@ -12,6 +18,10 @@ export const POST = withRateLimitTier(async (req: NextRequest) => {
|
|
|
12
18
|
return NextResponse.json({ error: 'User ID is required' }, { status: 400 })
|
|
13
19
|
}
|
|
14
20
|
|
|
21
|
+
if (userId !== authResult.user.id) {
|
|
22
|
+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
if (!metadata || typeof metadata !== 'object') {
|
|
16
26
|
return NextResponse.json({ error: 'Metadata is required' }, { status: 400 })
|
|
17
27
|
}
|
|
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
2
2
|
import { getTypedSession } from '@nextsparkjs/core/lib/auth';
|
|
3
3
|
import { queryWithRLS } from '@nextsparkjs/core/lib/db';
|
|
4
4
|
import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit';
|
|
5
|
+
import { getBillingGateway } from '@nextsparkjs/core/lib/billing/gateways/factory';
|
|
5
6
|
|
|
6
7
|
interface SubscriptionResult {
|
|
7
8
|
id: string;
|
|
@@ -23,6 +24,7 @@ interface SubscriptionResult {
|
|
|
23
24
|
canceledAt: string | null;
|
|
24
25
|
cancelAtPeriodEnd: boolean;
|
|
25
26
|
externalSubscriptionId: string | null;
|
|
27
|
+
paymentProvider: string | null;
|
|
26
28
|
createdAt: string;
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -181,6 +183,7 @@ export const GET = withRateLimitTier(async (request: NextRequest) => {
|
|
|
181
183
|
s."canceledAt",
|
|
182
184
|
s."cancelAtPeriodEnd",
|
|
183
185
|
s."externalSubscriptionId",
|
|
186
|
+
s."paymentProvider",
|
|
184
187
|
s."createdAt"
|
|
185
188
|
FROM "subscriptions" s
|
|
186
189
|
LEFT JOIN "teams" t ON s."teamId" = t.id
|
|
@@ -253,6 +256,8 @@ export const GET = withRateLimitTier(async (request: NextRequest) => {
|
|
|
253
256
|
canceledAt: sub.canceledAt,
|
|
254
257
|
cancelAtPeriodEnd: sub.cancelAtPeriodEnd,
|
|
255
258
|
externalSubscriptionId: sub.externalSubscriptionId,
|
|
259
|
+
paymentProvider: sub.paymentProvider,
|
|
260
|
+
providerDashboardUrl: getBillingGateway().getSubscriptionDashboardUrl(sub.externalSubscriptionId),
|
|
256
261
|
createdAt: sub.createdAt
|
|
257
262
|
}));
|
|
258
263
|
|
|
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
2
2
|
import { getTypedSession } from '@nextsparkjs/core/lib/auth';
|
|
3
3
|
import { queryWithRLS } from '@nextsparkjs/core/lib/db';
|
|
4
4
|
import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit';
|
|
5
|
+
import { getBillingGateway } from '@nextsparkjs/core/lib/billing/gateways/factory';
|
|
5
6
|
|
|
6
7
|
interface TeamResult {
|
|
7
8
|
id: string;
|
|
@@ -37,6 +38,7 @@ interface SubscriptionResult {
|
|
|
37
38
|
cancelAtPeriodEnd: boolean;
|
|
38
39
|
externalSubscriptionId: string | null;
|
|
39
40
|
externalCustomerId: string | null;
|
|
41
|
+
paymentProvider: string | null;
|
|
40
42
|
createdAt: string;
|
|
41
43
|
}
|
|
42
44
|
|
|
@@ -144,6 +146,7 @@ export const GET = withRateLimitTier(async (
|
|
|
144
146
|
s."cancelAtPeriodEnd",
|
|
145
147
|
s."externalSubscriptionId",
|
|
146
148
|
s."externalCustomerId",
|
|
149
|
+
s."paymentProvider",
|
|
147
150
|
s."createdAt"
|
|
148
151
|
FROM "subscriptions" s
|
|
149
152
|
LEFT JOIN "plans" p ON s."planId" = p.id
|
|
@@ -242,6 +245,9 @@ export const GET = withRateLimitTier(async (
|
|
|
242
245
|
cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
|
|
243
246
|
externalSubscriptionId: subscription.externalSubscriptionId,
|
|
244
247
|
externalCustomerId: subscription.externalCustomerId,
|
|
248
|
+
paymentProvider: subscription.paymentProvider,
|
|
249
|
+
providerName: getBillingGateway().getProviderName(),
|
|
250
|
+
providerDashboardUrl: getBillingGateway().getSubscriptionDashboardUrl(subscription.externalSubscriptionId),
|
|
245
251
|
createdAt: subscription.createdAt
|
|
246
252
|
} : null,
|
|
247
253
|
billingHistory: billingEventsResult.map((event) => ({
|