archetecture security fix
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
export declare function requireActiveSubscription(req: Request, res: Response, next: NextFunction): Promise<Response<any, Record<string, any>> | undefined>;
|
||||
export declare function requireFullAccess(req: Request, res: Response, next: NextFunction): Promise<Response<any, Record<string, any>> | undefined>;
|
||||
//# sourceMappingURL=subscription.entitlement.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"subscription.entitlement.d.ts","sourceRoot":"","sources":["../../../src/modules/subscriptions/subscription.entitlement.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAIzD,wBAAsB,yBAAyB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,2DAoB9F;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,2DAoBtF"}
|
||||
@@ -0,0 +1,47 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.requireActiveSubscription = requireActiveSubscription;
|
||||
exports.requireFullAccess = requireFullAccess;
|
||||
const prisma_1 = require("../../lib/prisma");
|
||||
const subscription_policy_1 = require("./subscription.policy");
|
||||
async function requireActiveSubscription(req, res, next) {
|
||||
const companyId = req.companyId;
|
||||
if (!companyId)
|
||||
return res.status(401).json({ error: 'unauthenticated' });
|
||||
const sub = await prisma_1.prisma.subscription.findUnique({ where: { companyId } });
|
||||
const status = sub?.status ?? 'EXPIRED';
|
||||
const accessLevel = (0, subscription_policy_1.getAccessLevel)(status);
|
||||
if (!(0, subscription_policy_1.hasAnyAccess)(status)) {
|
||||
return res.status(403).json({
|
||||
error: 'subscription_required',
|
||||
message: 'Your subscription has ended. Please reactivate to continue.',
|
||||
subscriptionStatus: status,
|
||||
accessLevel,
|
||||
});
|
||||
}
|
||||
;
|
||||
req.subscriptionStatus = status;
|
||||
req.accessLevel = accessLevel;
|
||||
next();
|
||||
}
|
||||
async function requireFullAccess(req, res, next) {
|
||||
const companyId = req.companyId;
|
||||
if (!companyId)
|
||||
return res.status(401).json({ error: 'unauthenticated' });
|
||||
const sub = await prisma_1.prisma.subscription.findUnique({ where: { companyId } });
|
||||
const status = sub?.status ?? 'EXPIRED';
|
||||
const accessLevel = (0, subscription_policy_1.getAccessLevel)(status);
|
||||
if (accessLevel !== 'full') {
|
||||
return res.status(403).json({
|
||||
error: 'insufficient_access',
|
||||
message: 'This action requires an active subscription.',
|
||||
subscriptionStatus: status,
|
||||
accessLevel,
|
||||
});
|
||||
}
|
||||
;
|
||||
req.subscriptionStatus = status;
|
||||
req.accessLevel = accessLevel;
|
||||
next();
|
||||
}
|
||||
//# sourceMappingURL=subscription.entitlement.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"subscription.entitlement.js","sourceRoot":"","sources":["../../../src/modules/subscriptions/subscription.entitlement.ts"],"names":[],"mappings":";;AAIA,8DAoBC;AAED,8CAoBC;AA7CD,6CAAyC;AACzC,+DAAoE;AAE7D,KAAK,UAAU,yBAAyB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IAC7F,MAAM,SAAS,GAAG,GAAG,CAAC,SAAS,CAAA;IAC/B,IAAI,CAAC,SAAS;QAAE,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC,CAAA;IAEzE,MAAM,GAAG,GAAG,MAAM,eAAM,CAAC,YAAY,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,EAAE,CAAC,CAAA;IAC1E,MAAM,MAAM,GAAG,GAAG,EAAE,MAAM,IAAI,SAAS,CAAA;IACvC,MAAM,WAAW,GAAG,IAAA,oCAAc,EAAC,MAAM,CAAC,CAAA;IAE1C,IAAI,CAAC,IAAA,kCAAY,EAAC,MAAM,CAAC,EAAE,CAAC;QAC1B,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YAC1B,KAAK,EAAE,uBAAuB;YAC9B,OAAO,EAAE,6DAA6D;YACtE,kBAAkB,EAAE,MAAM;YAC1B,WAAW;SACZ,CAAC,CAAA;IACJ,CAAC;IAED,CAAC;IAAC,GAAW,CAAC,kBAAkB,GAAG,MAAM,CACxC;IAAC,GAAW,CAAC,WAAW,GAAG,WAAW,CAAA;IACvC,IAAI,EAAE,CAAA;AACR,CAAC;AAEM,KAAK,UAAU,iBAAiB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACrF,MAAM,SAAS,GAAG,GAAG,CAAC,SAAS,CAAA;IAC/B,IAAI,CAAC,SAAS;QAAE,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC,CAAA;IAEzE,MAAM,GAAG,GAAG,MAAM,eAAM,CAAC,YAAY,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,EAAE,CAAC,CAAA;IAC1E,MAAM,MAAM,GAAG,GAAG,EAAE,MAAM,IAAI,SAAS,CAAA;IACvC,MAAM,WAAW,GAAG,IAAA,oCAAc,EAAC,MAAM,CAAC,CAAA;IAE1C,IAAI,WAAW,KAAK,MAAM,EAAE,CAAC;QAC3B,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YAC1B,KAAK,EAAE,qBAAqB;YAC5B,OAAO,EAAE,8CAA8C;YACvD,kBAAkB,EAAE,MAAM;YAC1B,WAAW;SACZ,CAAC,CAAA;IACJ,CAAC;IAED,CAAC;IAAC,GAAW,CAAC,kBAAkB,GAAG,MAAM,CACxC;IAAC,GAAW,CAAC,WAAW,GAAG,WAAW,CAAA;IACvC,IAAI,EAAE,CAAA;AACR,CAAC"}
|
||||
@@ -0,0 +1,31 @@
|
||||
export type AccessLevel = 'full' | 'limited' | 'read_only' | 'none';
|
||||
export declare const SUBSCRIPTION_POLICY: {
|
||||
readonly trial: {
|
||||
readonly durationDays: 14;
|
||||
readonly requiresPaymentMethod: false;
|
||||
readonly oneTrialPerCompany: true;
|
||||
};
|
||||
readonly payment: {
|
||||
readonly paymentPendingTimeoutDays: 7;
|
||||
readonly retryScheduleDays: readonly [0, 3, 6, 10, 14];
|
||||
readonly maxRetryAttempts: 5;
|
||||
};
|
||||
readonly pastDue: {
|
||||
readonly timeoutDays: 7;
|
||||
};
|
||||
readonly suspension: {
|
||||
readonly cancelAfterDays: 16;
|
||||
};
|
||||
readonly notifications: {
|
||||
readonly trialEndingDaysBefore: readonly [7, 3, 1];
|
||||
readonly paymentFailureDaysAfter: readonly [0, 3, 6];
|
||||
readonly pastDueDaysAfter: readonly [7];
|
||||
readonly suspensionDaysAfter: readonly [14];
|
||||
readonly cancellationDaysAfter: readonly [30];
|
||||
};
|
||||
};
|
||||
export declare const ACCESS_MAP: Record<string, AccessLevel>;
|
||||
export declare function getAccessLevel(status: string): AccessLevel;
|
||||
export declare function hasFullAccess(status: string): boolean;
|
||||
export declare function hasAnyAccess(status: string): boolean;
|
||||
//# sourceMappingURL=subscription.policy.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"subscription.policy.d.ts","sourceRoot":"","sources":["../../../src/modules/subscriptions/subscription.policy.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,WAAW,GAAG,MAAM,CAAA;AAEnE,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;CAwBtB,CAAA;AAEV,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAUlD,CAAA;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAE1D;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEpD"}
|
||||
@@ -0,0 +1,52 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ACCESS_MAP = exports.SUBSCRIPTION_POLICY = void 0;
|
||||
exports.getAccessLevel = getAccessLevel;
|
||||
exports.hasFullAccess = hasFullAccess;
|
||||
exports.hasAnyAccess = hasAnyAccess;
|
||||
exports.SUBSCRIPTION_POLICY = {
|
||||
trial: {
|
||||
durationDays: 14,
|
||||
requiresPaymentMethod: false, // set true when payment capture is mandatory
|
||||
oneTrialPerCompany: true,
|
||||
},
|
||||
payment: {
|
||||
paymentPendingTimeoutDays: 7,
|
||||
retryScheduleDays: [0, 3, 6, 10, 14],
|
||||
maxRetryAttempts: 5,
|
||||
},
|
||||
pastDue: {
|
||||
timeoutDays: 7,
|
||||
},
|
||||
suspension: {
|
||||
cancelAfterDays: 16,
|
||||
},
|
||||
notifications: {
|
||||
trialEndingDaysBefore: [7, 3, 1],
|
||||
paymentFailureDaysAfter: [0, 3, 6],
|
||||
pastDueDaysAfter: [7],
|
||||
suspensionDaysAfter: [14],
|
||||
cancellationDaysAfter: [30],
|
||||
},
|
||||
};
|
||||
exports.ACCESS_MAP = {
|
||||
TRIALING: 'full',
|
||||
ACTIVE: 'full',
|
||||
PAYMENT_PENDING: 'full',
|
||||
UNPAID: 'full',
|
||||
PAST_DUE: 'limited',
|
||||
SUSPENDED: 'read_only',
|
||||
CANCELLED: 'none',
|
||||
EXPIRED: 'none',
|
||||
PAUSED: 'read_only',
|
||||
};
|
||||
function getAccessLevel(status) {
|
||||
return exports.ACCESS_MAP[status] ?? 'none';
|
||||
}
|
||||
function hasFullAccess(status) {
|
||||
return getAccessLevel(status) === 'full';
|
||||
}
|
||||
function hasAnyAccess(status) {
|
||||
return getAccessLevel(status) !== 'none';
|
||||
}
|
||||
//# sourceMappingURL=subscription.policy.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"subscription.policy.js","sourceRoot":"","sources":["../../../src/modules/subscriptions/subscription.policy.ts"],"names":[],"mappings":";;;AAwCA,wCAEC;AAED,sCAEC;AAED,oCAEC;AAhDY,QAAA,mBAAmB,GAAG;IACjC,KAAK,EAAE;QACL,YAAY,EAAE,EAAE;QAChB,qBAAqB,EAAE,KAAK,EAAE,6CAA6C;QAC3E,kBAAkB,EAAE,IAAI;KACzB;IACD,OAAO,EAAE;QACP,yBAAyB,EAAE,CAAC;QAC5B,iBAAiB,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC;QACpC,gBAAgB,EAAE,CAAC;KACpB;IACD,OAAO,EAAE;QACP,WAAW,EAAE,CAAC;KACf;IACD,UAAU,EAAE;QACV,eAAe,EAAE,EAAE;KACpB;IACD,aAAa,EAAE;QACb,qBAAqB,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAChC,uBAAuB,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAClC,gBAAgB,EAAE,CAAC,CAAC,CAAC;QACrB,mBAAmB,EAAE,CAAC,EAAE,CAAC;QACzB,qBAAqB,EAAE,CAAC,EAAE,CAAC;KAC5B;CACO,CAAA;AAEG,QAAA,UAAU,GAAgC;IACrD,QAAQ,EAAS,MAAM;IACvB,MAAM,EAAW,MAAM;IACvB,eAAe,EAAE,MAAM;IACvB,MAAM,EAAW,MAAM;IACvB,QAAQ,EAAS,SAAS;IAC1B,SAAS,EAAQ,WAAW;IAC5B,SAAS,EAAQ,MAAM;IACvB,OAAO,EAAU,MAAM;IACvB,MAAM,EAAW,WAAW;CAC7B,CAAA;AAED,SAAgB,cAAc,CAAC,MAAc;IAC3C,OAAO,kBAAU,CAAC,MAAM,CAAC,IAAI,MAAM,CAAA;AACrC,CAAC;AAED,SAAgB,aAAa,CAAC,MAAc;IAC1C,OAAO,cAAc,CAAC,MAAM,CAAC,KAAK,MAAM,CAAA;AAC1C,CAAC;AAED,SAAgB,YAAY,CAAC,MAAc;IACzC,OAAO,cAAc,CAAC,MAAM,CAAC,KAAK,MAAM,CAAA;AAC1C,CAAC"}
|
||||
@@ -0,0 +1,59 @@
|
||||
export declare function findByCompany(companyId: string): any;
|
||||
export declare function findById(id: string): any;
|
||||
export declare function findInvoices(companyId: string): any;
|
||||
export declare function findInvoiceByAmanpay(transactionId: string): any;
|
||||
export declare function findInvoiceByPaypal(captureId: string): any;
|
||||
export declare function findInvoiceByPaypalForCompany(paypalOrderId: string, companyId: string): any;
|
||||
export declare function findInvoiceById(id: string): any;
|
||||
export declare function findOrCreateSubscription(companyId: string, plan: string, billingPeriod: string, currency: string): Promise<any>;
|
||||
export declare function activateSubscription(id: string, periodEnd: Date): any;
|
||||
export declare function startTrial(companyId: string, plan: string, billingPeriod: string, currency: string, trialEndAt: Date): any;
|
||||
export declare function setPaymentPending(id: string): any;
|
||||
export declare function setPastDue(id: string): any;
|
||||
export declare function setSuspended(id: string): any;
|
||||
export declare function setExpired(id: string): any;
|
||||
export declare function setCancelled(id: string, immediate: boolean): any;
|
||||
export declare function setCancelAtPeriodEnd(companyId: string, value: boolean): any;
|
||||
export declare function incrementRetryCount(id: string): any;
|
||||
export declare function updatePlan(companyId: string, data: {
|
||||
plan: any;
|
||||
billingPeriod: any;
|
||||
currency: string;
|
||||
}): any;
|
||||
export declare function createInvoice(data: {
|
||||
companyId: string;
|
||||
subscriptionId: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
paymentProvider: string;
|
||||
amanpayTransactionId?: string | null;
|
||||
paypalCaptureId?: string | null;
|
||||
dueAt?: Date | null;
|
||||
}): any;
|
||||
export declare function markInvoicePaid(id: string): any;
|
||||
export declare function markInvoiceFailed(id: string): any;
|
||||
export declare function markInvoiceVoided(id: string): any;
|
||||
export declare function updateInvoicePaypal(id: string, captureId: string): any;
|
||||
export declare function createPaymentAttempt(data: {
|
||||
invoiceId: string;
|
||||
subscriptionId: string;
|
||||
providerPaymentId?: string | null;
|
||||
status: string;
|
||||
failureCode?: string | null;
|
||||
failureMessage?: string | null;
|
||||
}): any;
|
||||
export declare function getPaymentAttempts(invoiceId: string): any;
|
||||
export declare function createEvent(data: {
|
||||
subscriptionId: string;
|
||||
companyId: string;
|
||||
eventType: string;
|
||||
source?: string;
|
||||
payload?: object;
|
||||
}): any;
|
||||
export declare function getEvents(subscriptionId: string): any;
|
||||
export declare function findPaymentPendingTimedOut(cutoffDate: Date): any;
|
||||
export declare function findPastDueTimedOut(cutoffDate: Date): any;
|
||||
export declare function findSuspendedTimedOut(cutoffDate: Date): any;
|
||||
export declare function findTrialsEndedWithoutConversion(): any;
|
||||
export declare function findSubscriptionsToExpireAtPeriodEnd(): any;
|
||||
//# sourceMappingURL=subscription.repo.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"subscription.repo.d.ts","sourceRoot":"","sources":["../../../src/modules/subscriptions/subscription.repo.ts"],"names":[],"mappings":"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"}
|
||||
@@ -0,0 +1,301 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.findByCompany = findByCompany;
|
||||
exports.findById = findById;
|
||||
exports.findInvoices = findInvoices;
|
||||
exports.findInvoiceByAmanpay = findInvoiceByAmanpay;
|
||||
exports.findInvoiceByPaypal = findInvoiceByPaypal;
|
||||
exports.findInvoiceByPaypalForCompany = findInvoiceByPaypalForCompany;
|
||||
exports.findInvoiceById = findInvoiceById;
|
||||
exports.findOrCreateSubscription = findOrCreateSubscription;
|
||||
exports.activateSubscription = activateSubscription;
|
||||
exports.startTrial = startTrial;
|
||||
exports.setPaymentPending = setPaymentPending;
|
||||
exports.setPastDue = setPastDue;
|
||||
exports.setSuspended = setSuspended;
|
||||
exports.setExpired = setExpired;
|
||||
exports.setCancelled = setCancelled;
|
||||
exports.setCancelAtPeriodEnd = setCancelAtPeriodEnd;
|
||||
exports.incrementRetryCount = incrementRetryCount;
|
||||
exports.updatePlan = updatePlan;
|
||||
exports.createInvoice = createInvoice;
|
||||
exports.markInvoicePaid = markInvoicePaid;
|
||||
exports.markInvoiceFailed = markInvoiceFailed;
|
||||
exports.markInvoiceVoided = markInvoiceVoided;
|
||||
exports.updateInvoicePaypal = updateInvoicePaypal;
|
||||
exports.createPaymentAttempt = createPaymentAttempt;
|
||||
exports.getPaymentAttempts = getPaymentAttempts;
|
||||
exports.createEvent = createEvent;
|
||||
exports.getEvents = getEvents;
|
||||
exports.findPaymentPendingTimedOut = findPaymentPendingTimedOut;
|
||||
exports.findPastDueTimedOut = findPastDueTimedOut;
|
||||
exports.findSuspendedTimedOut = findSuspendedTimedOut;
|
||||
exports.findTrialsEndedWithoutConversion = findTrialsEndedWithoutConversion;
|
||||
exports.findSubscriptionsToExpireAtPeriodEnd = findSubscriptionsToExpireAtPeriodEnd;
|
||||
const prisma_1 = require("../../lib/prisma");
|
||||
// ─── Subscription queries ─────────────────────────────────────
|
||||
function findByCompany(companyId) {
|
||||
return prisma_1.prisma.subscription.findUnique({
|
||||
where: { companyId },
|
||||
include: {
|
||||
invoices: { orderBy: { createdAt: 'desc' }, take: 12 },
|
||||
events: { orderBy: { occurredAt: 'desc' }, take: 20 },
|
||||
},
|
||||
});
|
||||
}
|
||||
function findById(id) {
|
||||
return prisma_1.prisma.subscription.findUnique({
|
||||
where: { id },
|
||||
include: { company: true },
|
||||
});
|
||||
}
|
||||
function findInvoices(companyId) {
|
||||
return prisma_1.prisma.subscriptionInvoice.findMany({
|
||||
where: { companyId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 50,
|
||||
});
|
||||
}
|
||||
function findInvoiceByAmanpay(transactionId) {
|
||||
return prisma_1.prisma.subscriptionInvoice.findFirst({
|
||||
where: { amanpayTransactionId: transactionId },
|
||||
include: { subscription: true },
|
||||
});
|
||||
}
|
||||
function findInvoiceByPaypal(captureId) {
|
||||
return prisma_1.prisma.subscriptionInvoice.findFirst({
|
||||
where: { paypalCaptureId: captureId },
|
||||
include: { subscription: true },
|
||||
});
|
||||
}
|
||||
function findInvoiceByPaypalForCompany(paypalOrderId, companyId) {
|
||||
return prisma_1.prisma.subscriptionInvoice.findFirstOrThrow({
|
||||
where: { paypalCaptureId: paypalOrderId, companyId },
|
||||
include: { subscription: true },
|
||||
});
|
||||
}
|
||||
function findInvoiceById(id) {
|
||||
return prisma_1.prisma.subscriptionInvoice.findUniqueOrThrow({
|
||||
where: { id },
|
||||
include: { subscription: true },
|
||||
});
|
||||
}
|
||||
// ─── Subscription mutations ───────────────────────────────────
|
||||
async function findOrCreateSubscription(companyId, plan, billingPeriod, currency) {
|
||||
const existing = await prisma_1.prisma.subscription.findUnique({ where: { companyId } });
|
||||
if (existing)
|
||||
return existing;
|
||||
return prisma_1.prisma.subscription.create({
|
||||
data: { companyId, plan: plan, billingPeriod: billingPeriod, currency, status: 'PAYMENT_PENDING' },
|
||||
});
|
||||
}
|
||||
function activateSubscription(id, periodEnd) {
|
||||
return prisma_1.prisma.subscription.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'ACTIVE',
|
||||
currentPeriodStart: new Date(),
|
||||
currentPeriodEnd: periodEnd,
|
||||
paymentPendingSince: null,
|
||||
paymentDueAt: null,
|
||||
pastDueSince: null,
|
||||
suspendedAt: null,
|
||||
retryCount: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
function startTrial(companyId, plan, billingPeriod, currency, trialEndAt) {
|
||||
return prisma_1.prisma.subscription.upsert({
|
||||
where: { companyId },
|
||||
update: {
|
||||
plan: plan,
|
||||
billingPeriod: billingPeriod,
|
||||
currency,
|
||||
status: 'TRIALING',
|
||||
trialStartAt: new Date(),
|
||||
trialEndAt,
|
||||
trialUsed: true,
|
||||
},
|
||||
create: {
|
||||
companyId,
|
||||
plan: plan,
|
||||
billingPeriod: billingPeriod,
|
||||
currency,
|
||||
status: 'TRIALING',
|
||||
trialStartAt: new Date(),
|
||||
trialEndAt,
|
||||
trialUsed: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
function setPaymentPending(id) {
|
||||
const now = new Date();
|
||||
const dueAt = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
||||
return prisma_1.prisma.subscription.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'PAYMENT_PENDING',
|
||||
paymentPendingSince: now,
|
||||
paymentDueAt: dueAt,
|
||||
},
|
||||
});
|
||||
}
|
||||
function setPastDue(id) {
|
||||
return prisma_1.prisma.subscription.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'PAST_DUE',
|
||||
pastDueSince: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
function setSuspended(id) {
|
||||
return prisma_1.prisma.subscription.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'SUSPENDED',
|
||||
suspendedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
function setExpired(id) {
|
||||
return prisma_1.prisma.subscription.update({
|
||||
where: { id },
|
||||
data: { status: 'EXPIRED', endedAt: new Date() },
|
||||
});
|
||||
}
|
||||
function setCancelled(id, immediate) {
|
||||
if (immediate) {
|
||||
return prisma_1.prisma.subscription.update({
|
||||
where: { id },
|
||||
data: { status: 'CANCELLED', cancelledAt: new Date(), endedAt: new Date() },
|
||||
});
|
||||
}
|
||||
return prisma_1.prisma.subscription.update({
|
||||
where: { id },
|
||||
data: { cancelAtPeriodEnd: true },
|
||||
});
|
||||
}
|
||||
function setCancelAtPeriodEnd(companyId, value) {
|
||||
return prisma_1.prisma.subscription.update({
|
||||
where: { companyId },
|
||||
data: { cancelAtPeriodEnd: value },
|
||||
});
|
||||
}
|
||||
function incrementRetryCount(id) {
|
||||
return prisma_1.prisma.subscription.update({
|
||||
where: { id },
|
||||
data: { retryCount: { increment: 1 } },
|
||||
});
|
||||
}
|
||||
function updatePlan(companyId, data) {
|
||||
return prisma_1.prisma.subscription.update({ where: { companyId }, data });
|
||||
}
|
||||
// ─── Invoice mutations ────────────────────────────────────────
|
||||
function createInvoice(data) {
|
||||
return prisma_1.prisma.subscriptionInvoice.create({
|
||||
data: { ...data, status: 'PENDING', paymentProvider: data.paymentProvider },
|
||||
});
|
||||
}
|
||||
function markInvoicePaid(id) {
|
||||
return prisma_1.prisma.subscriptionInvoice.update({
|
||||
where: { id },
|
||||
data: { status: 'PAID', paidAt: new Date(), failedAt: null },
|
||||
});
|
||||
}
|
||||
function markInvoiceFailed(id) {
|
||||
return prisma_1.prisma.subscriptionInvoice.update({
|
||||
where: { id },
|
||||
data: { status: 'FAILED', failedAt: new Date() },
|
||||
});
|
||||
}
|
||||
function markInvoiceVoided(id) {
|
||||
return prisma_1.prisma.subscriptionInvoice.update({
|
||||
where: { id },
|
||||
data: { status: 'VOIDED', voidedAt: new Date() },
|
||||
});
|
||||
}
|
||||
function updateInvoicePaypal(id, captureId) {
|
||||
return prisma_1.prisma.subscriptionInvoice.update({
|
||||
where: { id },
|
||||
data: { status: 'PAID', paidAt: new Date(), paypalCaptureId: captureId, failedAt: null },
|
||||
});
|
||||
}
|
||||
// ─── Payment attempts ─────────────────────────────────────────
|
||||
function createPaymentAttempt(data) {
|
||||
return prisma_1.prisma.paymentAttempt.create({
|
||||
data: { ...data, attemptedAt: new Date() },
|
||||
});
|
||||
}
|
||||
function getPaymentAttempts(invoiceId) {
|
||||
return prisma_1.prisma.paymentAttempt.findMany({
|
||||
where: { invoiceId },
|
||||
orderBy: { attemptedAt: 'desc' },
|
||||
});
|
||||
}
|
||||
// ─── Subscription events ──────────────────────────────────────
|
||||
function createEvent(data) {
|
||||
return prisma_1.prisma.subscriptionEvent.create({
|
||||
data: {
|
||||
...data,
|
||||
source: data.source ?? 'system',
|
||||
payload: (data.payload ?? {}),
|
||||
occurredAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
function getEvents(subscriptionId) {
|
||||
return prisma_1.prisma.subscriptionEvent.findMany({
|
||||
where: { subscriptionId },
|
||||
orderBy: { occurredAt: 'desc' },
|
||||
take: 50,
|
||||
});
|
||||
}
|
||||
// ─── Scheduled job helpers ────────────────────────────────────
|
||||
function findPaymentPendingTimedOut(cutoffDate) {
|
||||
return prisma_1.prisma.subscription.findMany({
|
||||
where: {
|
||||
status: 'PAYMENT_PENDING',
|
||||
paymentPendingSince: { lte: cutoffDate },
|
||||
},
|
||||
include: { company: true },
|
||||
});
|
||||
}
|
||||
function findPastDueTimedOut(cutoffDate) {
|
||||
return prisma_1.prisma.subscription.findMany({
|
||||
where: {
|
||||
status: 'PAST_DUE',
|
||||
pastDueSince: { lte: cutoffDate },
|
||||
},
|
||||
include: { company: true },
|
||||
});
|
||||
}
|
||||
function findSuspendedTimedOut(cutoffDate) {
|
||||
return prisma_1.prisma.subscription.findMany({
|
||||
where: {
|
||||
status: 'SUSPENDED',
|
||||
suspendedAt: { lte: cutoffDate },
|
||||
},
|
||||
include: { company: true },
|
||||
});
|
||||
}
|
||||
function findTrialsEndedWithoutConversion() {
|
||||
return prisma_1.prisma.subscription.findMany({
|
||||
where: {
|
||||
status: 'TRIALING',
|
||||
trialEndAt: { lte: new Date() },
|
||||
},
|
||||
include: { company: { include: { employees: { where: { role: 'OWNER' }, take: 1 } } } },
|
||||
});
|
||||
}
|
||||
function findSubscriptionsToExpireAtPeriodEnd() {
|
||||
return prisma_1.prisma.subscription.findMany({
|
||||
where: {
|
||||
status: 'ACTIVE',
|
||||
cancelAtPeriodEnd: true,
|
||||
currentPeriodEnd: { lte: new Date() },
|
||||
},
|
||||
include: { company: true },
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=subscription.repo.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,6 @@
|
||||
declare const publicRouter: import("express-serve-static-core").Router;
|
||||
declare const webhookRouter: import("express-serve-static-core").Router;
|
||||
declare const router: import("express-serve-static-core").Router;
|
||||
export default router;
|
||||
export { publicRouter as subscriptionPublicRouter, webhookRouter as subscriptionWebhookRouter };
|
||||
//# sourceMappingURL=subscription.routes.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"subscription.routes.d.ts","sourceRoot":"","sources":["../../../src/modules/subscriptions/subscription.routes.ts"],"names":[],"mappings":"AAmBA,QAAA,MAAM,YAAY,4CAAY,CAAA;AAC9B,QAAA,MAAM,aAAa,4CAAW,CAAA;AAC9B,QAAA,MAAM,MAAM,4CAAkB,CAAA;AA8G9B,eAAe,MAAM,CAAA;AACrB,OAAO,EAAE,YAAY,IAAI,wBAAwB,EAAE,aAAa,IAAI,yBAAyB,EAAE,CAAA"}
|
||||
@@ -0,0 +1,191 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.subscriptionWebhookRouter = exports.subscriptionPublicRouter = void 0;
|
||||
const express_1 = require("express");
|
||||
const requireCompanyAuth_1 = require("../../middleware/requireCompanyAuth");
|
||||
const requireTenant_1 = require("../../middleware/requireTenant");
|
||||
const requireRole_1 = require("../../middleware/requireRole");
|
||||
const validate_1 = require("../../http/validate");
|
||||
const respond_1 = require("../../http/respond");
|
||||
const webhooks_1 = require("../../http/webhooks");
|
||||
const amanpay = __importStar(require("../../services/amanpayService"));
|
||||
const paypal = __importStar(require("../../services/paypalService"));
|
||||
const service = __importStar(require("./subscription.service"));
|
||||
const subscription_schemas_1 = require("./subscription.schemas");
|
||||
const publicRouter = (0, express_1.Router)();
|
||||
exports.subscriptionPublicRouter = publicRouter;
|
||||
const webhookRouter = (0, express_1.Router)();
|
||||
exports.subscriptionWebhookRouter = webhookRouter;
|
||||
const router = (0, express_1.Router)();
|
||||
// ─── Public ────────────────────────────────────────────────────
|
||||
publicRouter.get('/plans', (_req, res, next) => {
|
||||
service.getPlans().then((d) => (0, respond_1.ok)(res, d)).catch(next);
|
||||
});
|
||||
publicRouter.get('/providers', (_req, res) => {
|
||||
(0, respond_1.ok)(res, service.getProviders());
|
||||
});
|
||||
publicRouter.get('/features', (_req, res, next) => {
|
||||
service.getPlanFeatures().then((d) => (0, respond_1.ok)(res, d)).catch(next);
|
||||
});
|
||||
// ─── Webhooks (no auth) ────────────────────────────────────────
|
||||
webhookRouter.post('/webhooks/amanpay', async (req, res, next) => {
|
||||
try {
|
||||
const rawBody = (0, webhooks_1.getRawBodyString)(req);
|
||||
const payload = (0, webhooks_1.parseRawJsonBody)(req);
|
||||
const signature = req.headers['x-amanpay-signature'] ?? '';
|
||||
if (!amanpay.isConfigured() || !amanpay.verifyWebhookSignature(rawBody, signature)) {
|
||||
return res.status(401).json({ error: 'invalid_signature' });
|
||||
}
|
||||
await service.handleAmanpayWebhook(payload, rawBody);
|
||||
res.json({ received: true });
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
webhookRouter.post('/webhooks/paypal', async (req, res, next) => {
|
||||
try {
|
||||
const rawBody = (0, webhooks_1.getRawBodyString)(req);
|
||||
const payload = (0, webhooks_1.parseRawJsonBody)(req);
|
||||
const isValid = await paypal.verifyWebhookEvent(req.headers, rawBody);
|
||||
if (!paypal.isConfigured() || !isValid)
|
||||
return res.status(401).json({ error: 'invalid_signature' });
|
||||
await service.handlePaypalWebhook(payload, rawBody);
|
||||
res.json({ received: true });
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// ─── PayPal capture (auth but no subscription check) ──────────
|
||||
router.post('/capture-paypal', requireCompanyAuth_1.requireCompanyAuth, requireTenant_1.requireTenant, async (req, res, next) => {
|
||||
try {
|
||||
const { paypalOrderId } = (0, validate_1.parseBody)(subscription_schemas_1.capturePaypalSchema, req);
|
||||
(0, respond_1.ok)(res, await service.capturePaypal(req.companyId, paypalOrderId));
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// ─── Authenticated ─────────────────────────────────────────────
|
||||
router.use(requireCompanyAuth_1.requireCompanyAuth, requireTenant_1.requireTenant);
|
||||
router.get('/me', async (req, res, next) => {
|
||||
try {
|
||||
(0, respond_1.ok)(res, await service.getSubscription(req.companyId));
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
router.get('/invoices', async (req, res, next) => {
|
||||
try {
|
||||
(0, respond_1.ok)(res, await service.getInvoices(req.companyId));
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
router.get('/events', async (req, res, next) => {
|
||||
try {
|
||||
(0, respond_1.ok)(res, await service.getEvents(req.companyId));
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
router.get('/entitlement', async (req, res, next) => {
|
||||
try {
|
||||
(0, respond_1.ok)(res, await service.getEntitlement(req.companyId));
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
router.post('/trial', (0, requireRole_1.requireRole)('OWNER'), async (req, res, next) => {
|
||||
try {
|
||||
const body = (0, validate_1.parseBody)(subscription_schemas_1.startTrialSchema, req);
|
||||
(0, respond_1.ok)(res, await service.startTrial(req.companyId, body.plan, body.billingPeriod, body.currency));
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
router.post('/checkout', (0, requireRole_1.requireRole)('OWNER'), async (req, res, next) => {
|
||||
try {
|
||||
const body = (0, validate_1.parseBody)(subscription_schemas_1.checkoutSchema, req);
|
||||
(0, respond_1.ok)(res, await service.checkout(req.companyId, body));
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
router.post('/reactivate', (0, requireRole_1.requireRole)('OWNER'), async (req, res, next) => {
|
||||
try {
|
||||
const body = (0, validate_1.parseBody)(subscription_schemas_1.reactivateSchema, req);
|
||||
(0, respond_1.ok)(res, await service.reactivate(req.companyId, body));
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
router.post('/change-plan', (0, requireRole_1.requireRole)('OWNER'), async (req, res, next) => {
|
||||
try {
|
||||
const body = (0, validate_1.parseBody)(subscription_schemas_1.changePlanSchema, req);
|
||||
(0, respond_1.ok)(res, await service.changePlan(req.companyId, body));
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
router.post('/cancel', (0, requireRole_1.requireRole)('OWNER'), async (req, res, next) => {
|
||||
try {
|
||||
const { mode, reason } = (0, validate_1.parseBody)(subscription_schemas_1.cancelSchema, req);
|
||||
(0, respond_1.ok)(res, await service.cancel(req.companyId, mode, reason));
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
router.post('/resume', (0, requireRole_1.requireRole)('OWNER'), async (req, res, next) => {
|
||||
try {
|
||||
(0, respond_1.ok)(res, await service.resume(req.companyId));
|
||||
}
|
||||
catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
exports.default = router;
|
||||
//# sourceMappingURL=subscription.routes.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,89 @@
|
||||
import { z } from 'zod';
|
||||
export declare const checkoutSchema: z.ZodObject<{
|
||||
plan: z.ZodEnum<["STARTER", "GROWTH", "PRO"]>;
|
||||
billingPeriod: z.ZodEnum<["MONTHLY", "ANNUAL"]>;
|
||||
currency: z.ZodLiteral<"MAD">;
|
||||
provider: z.ZodEnum<["AMANPAY", "PAYPAL"]>;
|
||||
successUrl: z.ZodString;
|
||||
failureUrl: z.ZodString;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
provider: "AMANPAY" | "PAYPAL";
|
||||
currency: "MAD";
|
||||
successUrl: string;
|
||||
failureUrl: string;
|
||||
plan: "STARTER" | "GROWTH" | "PRO";
|
||||
billingPeriod: "MONTHLY" | "ANNUAL";
|
||||
}, {
|
||||
provider: "AMANPAY" | "PAYPAL";
|
||||
currency: "MAD";
|
||||
successUrl: string;
|
||||
failureUrl: string;
|
||||
plan: "STARTER" | "GROWTH" | "PRO";
|
||||
billingPeriod: "MONTHLY" | "ANNUAL";
|
||||
}>;
|
||||
export declare const changePlanSchema: z.ZodObject<{
|
||||
plan: z.ZodEnum<["STARTER", "GROWTH", "PRO"]>;
|
||||
billingPeriod: z.ZodEnum<["MONTHLY", "ANNUAL"]>;
|
||||
currency: z.ZodEnum<["MAD", "EUR", "USD"]>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
currency: "MAD" | "EUR" | "USD";
|
||||
plan: "STARTER" | "GROWTH" | "PRO";
|
||||
billingPeriod: "MONTHLY" | "ANNUAL";
|
||||
}, {
|
||||
currency: "MAD" | "EUR" | "USD";
|
||||
plan: "STARTER" | "GROWTH" | "PRO";
|
||||
billingPeriod: "MONTHLY" | "ANNUAL";
|
||||
}>;
|
||||
export declare const capturePaypalSchema: z.ZodObject<{
|
||||
paypalOrderId: z.ZodString;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
paypalOrderId: string;
|
||||
}, {
|
||||
paypalOrderId: string;
|
||||
}>;
|
||||
export declare const startTrialSchema: z.ZodObject<{
|
||||
plan: z.ZodEnum<["STARTER", "GROWTH", "PRO"]>;
|
||||
billingPeriod: z.ZodEnum<["MONTHLY", "ANNUAL"]>;
|
||||
currency: z.ZodDefault<z.ZodLiteral<"MAD">>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
currency: "MAD";
|
||||
plan: "STARTER" | "GROWTH" | "PRO";
|
||||
billingPeriod: "MONTHLY" | "ANNUAL";
|
||||
}, {
|
||||
plan: "STARTER" | "GROWTH" | "PRO";
|
||||
billingPeriod: "MONTHLY" | "ANNUAL";
|
||||
currency?: "MAD" | undefined;
|
||||
}>;
|
||||
export declare const cancelSchema: z.ZodObject<{
|
||||
mode: z.ZodDefault<z.ZodEnum<["period_end", "immediate"]>>;
|
||||
reason: z.ZodOptional<z.ZodString>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
mode: "period_end" | "immediate";
|
||||
reason?: string | undefined;
|
||||
}, {
|
||||
reason?: string | undefined;
|
||||
mode?: "period_end" | "immediate" | undefined;
|
||||
}>;
|
||||
export declare const reactivateSchema: z.ZodObject<{
|
||||
plan: z.ZodEnum<["STARTER", "GROWTH", "PRO"]>;
|
||||
billingPeriod: z.ZodEnum<["MONTHLY", "ANNUAL"]>;
|
||||
currency: z.ZodLiteral<"MAD">;
|
||||
provider: z.ZodEnum<["AMANPAY", "PAYPAL"]>;
|
||||
successUrl: z.ZodString;
|
||||
failureUrl: z.ZodString;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
provider: "AMANPAY" | "PAYPAL";
|
||||
currency: "MAD";
|
||||
successUrl: string;
|
||||
failureUrl: string;
|
||||
plan: "STARTER" | "GROWTH" | "PRO";
|
||||
billingPeriod: "MONTHLY" | "ANNUAL";
|
||||
}, {
|
||||
provider: "AMANPAY" | "PAYPAL";
|
||||
currency: "MAD";
|
||||
successUrl: string;
|
||||
failureUrl: string;
|
||||
plan: "STARTER" | "GROWTH" | "PRO";
|
||||
billingPeriod: "MONTHLY" | "ANNUAL";
|
||||
}>;
|
||||
//# sourceMappingURL=subscription.schemas.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"subscription.schemas.d.ts","sourceRoot":"","sources":["../../../src/modules/subscriptions/subscription.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAOvB,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;EAOzB,CAAA;AAEF,eAAO,MAAM,gBAAgB;;;;;;;;;;;;EAI3B,CAAA;AAEF,eAAO,MAAM,mBAAmB;;;;;;EAE9B,CAAA;AAEF,eAAO,MAAM,gBAAgB;;;;;;;;;;;;EAI3B,CAAA;AAEF,eAAO,MAAM,YAAY;;;;;;;;;EAGvB,CAAA;AAEF,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;EAO3B,CAAA"}
|
||||
@@ -0,0 +1,42 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.reactivateSchema = exports.cancelSchema = exports.startTrialSchema = exports.capturePaypalSchema = exports.changePlanSchema = exports.checkoutSchema = void 0;
|
||||
const zod_1 = require("zod");
|
||||
const planEnum = zod_1.z.enum(['STARTER', 'GROWTH', 'PRO']);
|
||||
const billingPeriodEnum = zod_1.z.enum(['MONTHLY', 'ANNUAL']);
|
||||
const providerEnum = zod_1.z.enum(['AMANPAY', 'PAYPAL']);
|
||||
const currencyEnum = zod_1.z.enum(['MAD', 'EUR', 'USD']);
|
||||
exports.checkoutSchema = zod_1.z.object({
|
||||
plan: planEnum,
|
||||
billingPeriod: billingPeriodEnum,
|
||||
currency: zod_1.z.literal('MAD'),
|
||||
provider: providerEnum,
|
||||
successUrl: zod_1.z.string().url(),
|
||||
failureUrl: zod_1.z.string().url(),
|
||||
});
|
||||
exports.changePlanSchema = zod_1.z.object({
|
||||
plan: planEnum,
|
||||
billingPeriod: billingPeriodEnum,
|
||||
currency: currencyEnum,
|
||||
});
|
||||
exports.capturePaypalSchema = zod_1.z.object({
|
||||
paypalOrderId: zod_1.z.string(),
|
||||
});
|
||||
exports.startTrialSchema = zod_1.z.object({
|
||||
plan: planEnum,
|
||||
billingPeriod: billingPeriodEnum,
|
||||
currency: zod_1.z.literal('MAD').default('MAD'),
|
||||
});
|
||||
exports.cancelSchema = zod_1.z.object({
|
||||
mode: zod_1.z.enum(['period_end', 'immediate']).default('period_end'),
|
||||
reason: zod_1.z.string().max(500).optional(),
|
||||
});
|
||||
exports.reactivateSchema = zod_1.z.object({
|
||||
plan: planEnum,
|
||||
billingPeriod: billingPeriodEnum,
|
||||
currency: zod_1.z.literal('MAD'),
|
||||
provider: providerEnum,
|
||||
successUrl: zod_1.z.string().url(),
|
||||
failureUrl: zod_1.z.string().url(),
|
||||
});
|
||||
//# sourceMappingURL=subscription.schemas.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"subscription.schemas.js","sourceRoot":"","sources":["../../../src/modules/subscriptions/subscription.schemas.ts"],"names":[],"mappings":";;;AAAA,6BAAuB;AAEvB,MAAM,QAAQ,GAAY,OAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAA;AAC9D,MAAM,iBAAiB,GAAG,OAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAA;AACvD,MAAM,YAAY,GAAQ,OAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAA;AACvD,MAAM,YAAY,GAAQ,OAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAA;AAE1C,QAAA,cAAc,GAAG,OAAC,CAAC,MAAM,CAAC;IACrC,IAAI,EAAW,QAAQ;IACvB,aAAa,EAAE,iBAAiB;IAChC,QAAQ,EAAO,OAAC,CAAC,OAAO,CAAC,KAAK,CAAC;IAC/B,QAAQ,EAAO,YAAY;IAC3B,UAAU,EAAK,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IAC/B,UAAU,EAAK,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;CAChC,CAAC,CAAA;AAEW,QAAA,gBAAgB,GAAG,OAAC,CAAC,MAAM,CAAC;IACvC,IAAI,EAAW,QAAQ;IACvB,aAAa,EAAE,iBAAiB;IAChC,QAAQ,EAAO,YAAY;CAC5B,CAAC,CAAA;AAEW,QAAA,mBAAmB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC1C,aAAa,EAAE,OAAC,CAAC,MAAM,EAAE;CAC1B,CAAC,CAAA;AAEW,QAAA,gBAAgB,GAAG,OAAC,CAAC,MAAM,CAAC;IACvC,IAAI,EAAW,QAAQ;IACvB,aAAa,EAAE,iBAAiB;IAChC,QAAQ,EAAO,OAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;CAC/C,CAAC,CAAA;AAEW,QAAA,YAAY,GAAG,OAAC,CAAC,MAAM,CAAC;IACnC,IAAI,EAAI,OAAC,CAAC,IAAI,CAAC,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC;IACjE,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;CACvC,CAAC,CAAA;AAEW,QAAA,gBAAgB,GAAG,OAAC,CAAC,MAAM,CAAC;IACvC,IAAI,EAAW,QAAQ;IACvB,aAAa,EAAE,iBAAiB;IAChC,QAAQ,EAAO,OAAC,CAAC,OAAO,CAAC,KAAK,CAAC;IAC/B,QAAQ,EAAO,YAAY;IAC3B,UAAU,EAAK,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IAC/B,UAAU,EAAK,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;CAChC,CAAC,CAAA"}
|
||||
@@ -0,0 +1,81 @@
|
||||
export declare function addPeriod(date: Date, period: string): Date;
|
||||
export declare function getPlans(): Promise<Record<string, Record<string, Record<string, number>>>>;
|
||||
export declare function getProviders(): {
|
||||
amanpay: boolean;
|
||||
paypal: boolean;
|
||||
};
|
||||
export declare function getPlanFeatures(): any;
|
||||
export declare function getSubscription(companyId: string): any;
|
||||
export declare function getInvoices(companyId: string): any;
|
||||
export declare function getEvents(companyId: string): any;
|
||||
export declare function getEventsBySubscriptionId(subscriptionId: string): any;
|
||||
export declare function getEntitlement(companyId: string): Promise<{
|
||||
companyId: string;
|
||||
subscriptionId: any;
|
||||
subscriptionStatus: any;
|
||||
accessLevel: import("./subscription.policy").AccessLevel;
|
||||
plan: any;
|
||||
currentPeriodEnd: any;
|
||||
}>;
|
||||
export declare function startTrial(companyId: string, plan: string, billingPeriod: string, currency: string): Promise<any>;
|
||||
export declare function handleAmanpayWebhook(event: any, rawBody?: string | Buffer): Promise<{
|
||||
duplicate: boolean;
|
||||
result?: void | undefined;
|
||||
}>;
|
||||
export declare function handlePaypalWebhook(event: any, rawBody?: string | Buffer): Promise<{
|
||||
duplicate: boolean;
|
||||
result?: void | undefined;
|
||||
}>;
|
||||
export declare function checkout(companyId: string, body: {
|
||||
plan: 'STARTER' | 'GROWTH' | 'PRO';
|
||||
billingPeriod: 'MONTHLY' | 'ANNUAL';
|
||||
currency: 'MAD';
|
||||
provider: 'AMANPAY' | 'PAYPAL';
|
||||
successUrl: string;
|
||||
failureUrl: string;
|
||||
}): Promise<{
|
||||
invoice: any;
|
||||
checkoutUrl: string;
|
||||
}>;
|
||||
export declare function capturePaypal(companyId: string, paypalOrderId: string): Promise<{
|
||||
success: boolean;
|
||||
}>;
|
||||
export declare function changePlan(companyId: string, data: {
|
||||
plan: any;
|
||||
billingPeriod: any;
|
||||
currency: string;
|
||||
}): any;
|
||||
export declare function cancel(companyId: string, mode?: 'period_end' | 'immediate', reason?: string): Promise<any>;
|
||||
export declare function resume(companyId: string): Promise<any>;
|
||||
export declare function reactivate(companyId: string, body: {
|
||||
plan: 'STARTER' | 'GROWTH' | 'PRO';
|
||||
billingPeriod: 'MONTHLY' | 'ANNUAL';
|
||||
currency: 'MAD';
|
||||
provider: 'AMANPAY' | 'PAYPAL';
|
||||
successUrl: string;
|
||||
failureUrl: string;
|
||||
}): Promise<{
|
||||
invoice: any;
|
||||
checkoutUrl: string;
|
||||
}>;
|
||||
export declare function runTrialExpirationJob(): Promise<any>;
|
||||
export declare function runPaymentPendingTimeoutJob(): Promise<any>;
|
||||
export declare function runPastDueTimeoutJob(): Promise<any>;
|
||||
export declare function runSuspensionTimeoutJob(): Promise<any>;
|
||||
export declare function runPeriodEndCancellationJob(): Promise<any>;
|
||||
export declare function adminExtendTrial(subscriptionId: string, extraDays: number, adminId: string, reason: string): Promise<{
|
||||
trialEndAt: Date;
|
||||
}>;
|
||||
export declare function adminExtendGracePeriod(subscriptionId: string, extraDays: number, adminId: string, reason: string): Promise<{
|
||||
success: boolean;
|
||||
}>;
|
||||
export declare function adminCancelSubscription(subscriptionId: string, adminId: string, reason: string): Promise<{
|
||||
success: boolean;
|
||||
}>;
|
||||
export declare function adminReactivateSubscription(subscriptionId: string, adminId: string, reason: string): Promise<{
|
||||
success: boolean;
|
||||
}>;
|
||||
export declare function adminSuspendSubscription(subscriptionId: string, adminId: string, reason: string): Promise<{
|
||||
success: boolean;
|
||||
}>;
|
||||
//# sourceMappingURL=subscription.service.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"subscription.service.d.ts","sourceRoot":"","sources":["../../../src/modules/subscriptions/subscription.service.ts"],"names":[],"mappings":"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"}
|
||||
@@ -0,0 +1,502 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.addPeriod = addPeriod;
|
||||
exports.getPlans = getPlans;
|
||||
exports.getProviders = getProviders;
|
||||
exports.getPlanFeatures = getPlanFeatures;
|
||||
exports.getSubscription = getSubscription;
|
||||
exports.getInvoices = getInvoices;
|
||||
exports.getEvents = getEvents;
|
||||
exports.getEventsBySubscriptionId = getEventsBySubscriptionId;
|
||||
exports.getEntitlement = getEntitlement;
|
||||
exports.startTrial = startTrial;
|
||||
exports.handleAmanpayWebhook = handleAmanpayWebhook;
|
||||
exports.handlePaypalWebhook = handlePaypalWebhook;
|
||||
exports.checkout = checkout;
|
||||
exports.capturePaypal = capturePaypal;
|
||||
exports.changePlan = changePlan;
|
||||
exports.cancel = cancel;
|
||||
exports.resume = resume;
|
||||
exports.reactivate = reactivate;
|
||||
exports.runTrialExpirationJob = runTrialExpirationJob;
|
||||
exports.runPaymentPendingTimeoutJob = runPaymentPendingTimeoutJob;
|
||||
exports.runPastDueTimeoutJob = runPastDueTimeoutJob;
|
||||
exports.runSuspensionTimeoutJob = runSuspensionTimeoutJob;
|
||||
exports.runPeriodEndCancellationJob = runPeriodEndCancellationJob;
|
||||
exports.adminExtendTrial = adminExtendTrial;
|
||||
exports.adminExtendGracePeriod = adminExtendGracePeriod;
|
||||
exports.adminCancelSubscription = adminCancelSubscription;
|
||||
exports.adminReactivateSubscription = adminReactivateSubscription;
|
||||
exports.adminSuspendSubscription = adminSuspendSubscription;
|
||||
const types_1 = require("@rentaldrivego/types");
|
||||
const prisma_1 = require("../../lib/prisma");
|
||||
const errors_1 = require("../../http/errors");
|
||||
const amanpay = __importStar(require("../../services/amanpayService"));
|
||||
const paypal = __importStar(require("../../services/paypalService"));
|
||||
const repo = __importStar(require("./subscription.repo"));
|
||||
const subscription_policy_1 = require("./subscription.policy");
|
||||
const webhookIdempotency_1 = require("../../security/webhookIdempotency");
|
||||
// ─── Helpers ──────────────────────────────────────────────────
|
||||
function addPeriod(date, period) {
|
||||
const d = new Date(date);
|
||||
period === 'ANNUAL' ? d.setFullYear(d.getFullYear() + 1) : d.setMonth(d.getMonth() + 1);
|
||||
return d;
|
||||
}
|
||||
// ─── Plans & providers ────────────────────────────────────────
|
||||
async function getPlans() {
|
||||
const configs = await prisma_1.prisma.pricingConfig.findMany();
|
||||
if (configs.length === 0)
|
||||
return types_1.PLAN_PRICES;
|
||||
const result = {};
|
||||
for (const c of configs) {
|
||||
if (!result[c.plan])
|
||||
result[c.plan] = {};
|
||||
result[c.plan][c.billingPeriod] = { MAD: c.amount };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function getProviders() {
|
||||
return { amanpay: amanpay.isConfigured(), paypal: paypal.isConfigured() };
|
||||
}
|
||||
function getPlanFeatures() {
|
||||
return prisma_1.prisma.planFeature.findMany({
|
||||
orderBy: [{ plan: 'asc' }, { sortOrder: 'asc' }, { createdAt: 'asc' }],
|
||||
});
|
||||
}
|
||||
// ─── Reads ────────────────────────────────────────────────────
|
||||
function getSubscription(companyId) {
|
||||
return repo.findByCompany(companyId);
|
||||
}
|
||||
function getInvoices(companyId) {
|
||||
return repo.findInvoices(companyId);
|
||||
}
|
||||
function getEvents(companyId) {
|
||||
return repo.findByCompany(companyId).then((sub) => sub ? repo.getEvents(sub.id) : []);
|
||||
}
|
||||
function getEventsBySubscriptionId(subscriptionId) {
|
||||
return repo.getEvents(subscriptionId);
|
||||
}
|
||||
// ─── Entitlements ─────────────────────────────────────────────
|
||||
async function getEntitlement(companyId) {
|
||||
const sub = await repo.findByCompany(companyId);
|
||||
const status = sub?.status ?? 'EXPIRED';
|
||||
const accessLevel = (0, subscription_policy_1.getAccessLevel)(status);
|
||||
return {
|
||||
companyId,
|
||||
subscriptionId: sub?.id ?? null,
|
||||
subscriptionStatus: status,
|
||||
accessLevel,
|
||||
plan: sub?.plan ?? null,
|
||||
currentPeriodEnd: sub?.currentPeriodEnd ?? null,
|
||||
};
|
||||
}
|
||||
// ─── Trial management ─────────────────────────────────────────
|
||||
async function startTrial(companyId, plan, billingPeriod, currency) {
|
||||
if (subscription_policy_1.SUBSCRIPTION_POLICY.trial.oneTrialPerCompany) {
|
||||
const existing = await repo.findByCompany(companyId);
|
||||
if (existing?.trialUsed) {
|
||||
throw new errors_1.ValidationError('This company has already used its free trial');
|
||||
}
|
||||
}
|
||||
const trialEndAt = new Date(Date.now() + subscription_policy_1.SUBSCRIPTION_POLICY.trial.durationDays * 24 * 60 * 60 * 1000);
|
||||
const sub = await repo.startTrial(companyId, plan, billingPeriod, currency, trialEndAt);
|
||||
await repo.createEvent({
|
||||
subscriptionId: sub.id,
|
||||
companyId,
|
||||
eventType: 'trial.started',
|
||||
payload: { plan, billingPeriod, trialEndAt },
|
||||
});
|
||||
return sub;
|
||||
}
|
||||
// ─── Payment success (shared by all providers) ───────────────
|
||||
async function handlePaymentSuccess(subscriptionId, invoiceId) {
|
||||
const sub = await repo.findById(subscriptionId);
|
||||
if (!sub)
|
||||
return;
|
||||
await repo.markInvoicePaid(invoiceId);
|
||||
await repo.createPaymentAttempt({
|
||||
invoiceId,
|
||||
subscriptionId,
|
||||
status: 'succeeded',
|
||||
});
|
||||
const periodEnd = addPeriod(new Date(), sub.billingPeriod);
|
||||
await repo.activateSubscription(subscriptionId, periodEnd);
|
||||
await repo.createEvent({
|
||||
subscriptionId,
|
||||
companyId: sub.companyId,
|
||||
eventType: sub.status === 'TRIALING' ? 'trial.converted' : 'subscription.activated',
|
||||
source: 'webhook',
|
||||
payload: { invoiceId, periodEnd },
|
||||
});
|
||||
}
|
||||
// ─── Payment failure ──────────────────────────────────────────
|
||||
async function handlePaymentFailure(subscriptionId, invoiceId, failureCode, failureMessage) {
|
||||
const sub = await repo.findById(subscriptionId);
|
||||
if (!sub)
|
||||
return;
|
||||
await repo.markInvoiceFailed(invoiceId);
|
||||
await repo.createPaymentAttempt({
|
||||
invoiceId,
|
||||
subscriptionId,
|
||||
status: 'failed',
|
||||
failureCode,
|
||||
failureMessage,
|
||||
});
|
||||
await repo.incrementRetryCount(sub.id);
|
||||
if (sub.status !== 'PAYMENT_PENDING') {
|
||||
await repo.setPaymentPending(sub.id);
|
||||
await repo.createEvent({
|
||||
subscriptionId: sub.id,
|
||||
companyId: sub.companyId,
|
||||
eventType: 'subscription.payment_pending',
|
||||
source: 'webhook',
|
||||
payload: { invoiceId, failureCode },
|
||||
});
|
||||
}
|
||||
}
|
||||
// ─── Webhook handlers ─────────────────────────────────────────
|
||||
async function applyAmanpayWebhook(event) {
|
||||
const transactionId = event.transaction_id ?? event.id;
|
||||
const status = event.status?.toUpperCase();
|
||||
if (status === 'PAID' || status === 'SUCCEEDED') {
|
||||
const invoice = await repo.findInvoiceByAmanpay(transactionId);
|
||||
if (!invoice || invoice.status === 'PAID')
|
||||
return; // idempotent
|
||||
await handlePaymentSuccess(invoice.subscriptionId, invoice.id);
|
||||
}
|
||||
else if (status === 'FAILED' || status === 'DECLINED') {
|
||||
const invoice = await repo.findInvoiceByAmanpay(transactionId);
|
||||
if (!invoice || invoice.status === 'PAID')
|
||||
return;
|
||||
await handlePaymentFailure(invoice.subscriptionId, invoice.id, status, event.failure_reason);
|
||||
}
|
||||
}
|
||||
async function applyPaypalWebhook(event) {
|
||||
if (event.event_type === 'PAYMENT.CAPTURE.COMPLETED') {
|
||||
const captureId = event.resource?.id;
|
||||
const invoice = await repo.findInvoiceByPaypal(captureId);
|
||||
if (!invoice || invoice.status === 'PAID')
|
||||
return; // idempotent
|
||||
await handlePaymentSuccess(invoice.subscriptionId, invoice.id);
|
||||
}
|
||||
else if (event.event_type === 'PAYMENT.CAPTURE.DENIED') {
|
||||
const captureId = event.resource?.id;
|
||||
const invoice = await repo.findInvoiceByPaypal(captureId);
|
||||
if (!invoice || invoice.status === 'PAID')
|
||||
return;
|
||||
await handlePaymentFailure(invoice.subscriptionId, invoice.id, 'capture_denied');
|
||||
}
|
||||
}
|
||||
async function handleAmanpayWebhook(event, rawBody = JSON.stringify(event)) {
|
||||
return (0, webhookIdempotency_1.processWebhookOnce)({
|
||||
provider: 'amanpay:subscriptions',
|
||||
providerEventId: (0, webhookIdempotency_1.getWebhookEventId)('amanpay', event),
|
||||
eventType: String(event.status ?? 'unknown'),
|
||||
rawBody,
|
||||
handle: () => applyAmanpayWebhook(event),
|
||||
});
|
||||
}
|
||||
async function handlePaypalWebhook(event, rawBody = JSON.stringify(event)) {
|
||||
return (0, webhookIdempotency_1.processWebhookOnce)({
|
||||
provider: 'paypal:subscriptions',
|
||||
providerEventId: (0, webhookIdempotency_1.getWebhookEventId)('paypal', event),
|
||||
eventType: String(event.event_type ?? 'unknown'),
|
||||
rawBody,
|
||||
handle: () => applyPaypalWebhook(event),
|
||||
});
|
||||
}
|
||||
// ─── Checkout ─────────────────────────────────────────────────
|
||||
async function checkout(companyId, body) {
|
||||
const dbPrice = await prisma_1.prisma.pricingConfig.findUnique({
|
||||
where: { plan_billingPeriod: { plan: body.plan, billingPeriod: body.billingPeriod } },
|
||||
});
|
||||
const amount = dbPrice?.amount ?? types_1.PLAN_PRICES[body.plan]?.[body.billingPeriod]?.[body.currency];
|
||||
if (!amount)
|
||||
throw new errors_1.ValidationError('Invalid plan or billing period');
|
||||
const company = await prisma_1.prisma.company.findUniqueOrThrow({ where: { id: companyId } });
|
||||
const subscription = await repo.findOrCreateSubscription(companyId, body.plan, body.billingPeriod, body.currency);
|
||||
const orderId = `sub-${companyId}-${Date.now()}`;
|
||||
const description = `${body.plan} plan — ${body.billingPeriod}`;
|
||||
const webhookBase = process.env.API_URL ?? 'http://localhost:4000';
|
||||
let checkoutUrl;
|
||||
let amanpayTransactionId = null;
|
||||
let paypalCaptureId = null;
|
||||
if (body.provider === 'AMANPAY') {
|
||||
if (!amanpay.isConfigured())
|
||||
throw new errors_1.ValidationError('AmanPay is not configured on this platform');
|
||||
const result = await amanpay.createCheckout({
|
||||
amount, currency: body.currency, orderId, description,
|
||||
customerEmail: company.email, customerName: company.name,
|
||||
successUrl: body.successUrl, failureUrl: body.failureUrl,
|
||||
webhookUrl: `${webhookBase}/api/v1/subscriptions/webhooks/amanpay`,
|
||||
});
|
||||
checkoutUrl = result.checkoutUrl;
|
||||
amanpayTransactionId = result.transactionId;
|
||||
}
|
||||
else {
|
||||
if (!paypal.isConfigured())
|
||||
throw new errors_1.ValidationError('PayPal is not configured on this platform');
|
||||
const result = await paypal.createOrder({
|
||||
amount, currency: body.currency, orderId, description,
|
||||
returnUrl: body.successUrl, cancelUrl: body.failureUrl,
|
||||
});
|
||||
checkoutUrl = result.approveUrl;
|
||||
paypalCaptureId = result.orderId;
|
||||
}
|
||||
const dueAt = new Date(Date.now() + subscription_policy_1.SUBSCRIPTION_POLICY.payment.paymentPendingTimeoutDays * 24 * 60 * 60 * 1000);
|
||||
const invoice = await repo.createInvoice({
|
||||
companyId, subscriptionId: subscription.id, amount, currency: body.currency,
|
||||
paymentProvider: body.provider, amanpayTransactionId, paypalCaptureId, dueAt,
|
||||
});
|
||||
return { invoice, checkoutUrl };
|
||||
}
|
||||
async function capturePaypal(companyId, paypalOrderId) {
|
||||
const invoice = await repo.findInvoiceByPaypalForCompany(paypalOrderId, companyId);
|
||||
if (invoice.status === 'PAID')
|
||||
return { success: true }; // idempotent
|
||||
const capture = await paypal.captureOrder(paypalOrderId);
|
||||
const captureId = capture.purchase_units?.[0]?.payments?.captures?.[0]?.id ?? paypalOrderId;
|
||||
await repo.updateInvoicePaypal(invoice.id, captureId);
|
||||
const periodEnd = addPeriod(new Date(), invoice.subscription.billingPeriod);
|
||||
await repo.activateSubscription(invoice.subscriptionId, periodEnd);
|
||||
await repo.createEvent({
|
||||
subscriptionId: invoice.subscriptionId,
|
||||
companyId,
|
||||
eventType: 'subscription.activated',
|
||||
source: 'user',
|
||||
payload: { invoiceId: invoice.id },
|
||||
});
|
||||
return { success: true };
|
||||
}
|
||||
// ─── Plan changes ─────────────────────────────────────────────
|
||||
function changePlan(companyId, data) {
|
||||
return repo.updatePlan(companyId, data);
|
||||
}
|
||||
// ─── Cancellation ─────────────────────────────────────────────
|
||||
async function cancel(companyId, mode = 'period_end', reason) {
|
||||
const sub = await repo.findByCompany(companyId);
|
||||
if (!sub)
|
||||
throw new errors_1.ValidationError('No subscription found');
|
||||
const updated = await repo.setCancelled(sub.id, mode === 'immediate');
|
||||
await repo.createEvent({
|
||||
subscriptionId: sub.id,
|
||||
companyId,
|
||||
eventType: 'subscription.canceled',
|
||||
source: 'user',
|
||||
payload: { mode, reason: reason ?? null },
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
async function resume(companyId) {
|
||||
const sub = await repo.findByCompany(companyId);
|
||||
if (!sub)
|
||||
throw new errors_1.ValidationError('No subscription found');
|
||||
const updated = await repo.setCancelAtPeriodEnd(companyId, false);
|
||||
await repo.createEvent({
|
||||
subscriptionId: sub.id,
|
||||
companyId,
|
||||
eventType: 'subscription.resumed',
|
||||
source: 'user',
|
||||
payload: {},
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
// ─── Reactivation ────────────────────────────────────────────
|
||||
async function reactivate(companyId, body) {
|
||||
const sub = await repo.findByCompany(companyId);
|
||||
if (!sub)
|
||||
throw new errors_1.ValidationError('No subscription found');
|
||||
const allowedStatuses = ['CANCELLED', 'EXPIRED', 'SUSPENDED', 'PAST_DUE', 'PAYMENT_PENDING'];
|
||||
if (!allowedStatuses.includes(sub.status)) {
|
||||
throw new errors_1.ValidationError(`Cannot reactivate a subscription with status ${sub.status}`);
|
||||
}
|
||||
await repo.setPaymentPending(sub.id);
|
||||
await repo.createEvent({
|
||||
subscriptionId: sub.id,
|
||||
companyId,
|
||||
eventType: 'subscription.reactivated',
|
||||
source: 'user',
|
||||
payload: { plan: body.plan, billingPeriod: body.billingPeriod },
|
||||
});
|
||||
return checkout(companyId, body);
|
||||
}
|
||||
// ─── Scheduled job actions ────────────────────────────────────
|
||||
async function runTrialExpirationJob() {
|
||||
const expired = await repo.findTrialsEndedWithoutConversion();
|
||||
for (const sub of expired) {
|
||||
await repo.setExpired(sub.id);
|
||||
await repo.createEvent({
|
||||
subscriptionId: sub.id,
|
||||
companyId: sub.companyId,
|
||||
eventType: 'trial.expired',
|
||||
payload: { trialEndAt: sub.trialEndAt },
|
||||
});
|
||||
}
|
||||
return expired.length;
|
||||
}
|
||||
async function runPaymentPendingTimeoutJob() {
|
||||
const cutoff = new Date(Date.now() - subscription_policy_1.SUBSCRIPTION_POLICY.payment.paymentPendingTimeoutDays * 24 * 60 * 60 * 1000);
|
||||
const subs = await repo.findPaymentPendingTimedOut(cutoff);
|
||||
for (const sub of subs) {
|
||||
await repo.setPastDue(sub.id);
|
||||
await repo.createEvent({
|
||||
subscriptionId: sub.id,
|
||||
companyId: sub.companyId,
|
||||
eventType: 'subscription.past_due',
|
||||
payload: { paymentPendingSince: sub.paymentPendingSince },
|
||||
});
|
||||
}
|
||||
return subs.length;
|
||||
}
|
||||
async function runPastDueTimeoutJob() {
|
||||
const cutoff = new Date(Date.now() - subscription_policy_1.SUBSCRIPTION_POLICY.pastDue.timeoutDays * 24 * 60 * 60 * 1000);
|
||||
const subs = await repo.findPastDueTimedOut(cutoff);
|
||||
for (const sub of subs) {
|
||||
await repo.setSuspended(sub.id);
|
||||
await repo.createEvent({
|
||||
subscriptionId: sub.id,
|
||||
companyId: sub.companyId,
|
||||
eventType: 'subscription.suspended',
|
||||
payload: { pastDueSince: sub.pastDueSince },
|
||||
});
|
||||
}
|
||||
return subs.length;
|
||||
}
|
||||
async function runSuspensionTimeoutJob() {
|
||||
const cutoff = new Date(Date.now() - subscription_policy_1.SUBSCRIPTION_POLICY.suspension.cancelAfterDays * 24 * 60 * 60 * 1000);
|
||||
const subs = await repo.findSuspendedTimedOut(cutoff);
|
||||
for (const sub of subs) {
|
||||
await repo.setCancelled(sub.id, true);
|
||||
await repo.createEvent({
|
||||
subscriptionId: sub.id,
|
||||
companyId: sub.companyId,
|
||||
eventType: 'subscription.canceled',
|
||||
payload: { reason: 'suspension_timeout', suspendedAt: sub.suspendedAt },
|
||||
});
|
||||
}
|
||||
return subs.length;
|
||||
}
|
||||
async function runPeriodEndCancellationJob() {
|
||||
const subs = await repo.findSubscriptionsToExpireAtPeriodEnd();
|
||||
for (const sub of subs) {
|
||||
await repo.setCancelled(sub.id, true);
|
||||
await repo.createEvent({
|
||||
subscriptionId: sub.id,
|
||||
companyId: sub.companyId,
|
||||
eventType: 'subscription.canceled',
|
||||
payload: { reason: 'period_end', currentPeriodEnd: sub.currentPeriodEnd },
|
||||
});
|
||||
}
|
||||
return subs.length;
|
||||
}
|
||||
// ─── Admin overrides ──────────────────────────────────────────
|
||||
async function adminExtendTrial(subscriptionId, extraDays, adminId, reason) {
|
||||
const sub = await repo.findById(subscriptionId);
|
||||
if (!sub)
|
||||
throw new errors_1.ValidationError('Subscription not found');
|
||||
const currentEnd = sub.trialEndAt ?? new Date();
|
||||
const newEnd = new Date(currentEnd.getTime() + extraDays * 24 * 60 * 60 * 1000);
|
||||
await prisma_1.prisma.subscription.update({
|
||||
where: { id: subscriptionId },
|
||||
data: { trialEndAt: newEnd },
|
||||
});
|
||||
await repo.createEvent({
|
||||
subscriptionId,
|
||||
companyId: sub.companyId,
|
||||
eventType: 'admin.override_created',
|
||||
source: 'admin',
|
||||
payload: { action: 'extend_trial', extraDays, newEnd, adminId, reason },
|
||||
});
|
||||
return { trialEndAt: newEnd };
|
||||
}
|
||||
async function adminExtendGracePeriod(subscriptionId, extraDays, adminId, reason) {
|
||||
const sub = await repo.findById(subscriptionId);
|
||||
if (!sub)
|
||||
throw new errors_1.ValidationError('Subscription not found');
|
||||
await repo.setPaymentPending(sub.id);
|
||||
await repo.createEvent({
|
||||
subscriptionId,
|
||||
companyId: sub.companyId,
|
||||
eventType: 'admin.override_created',
|
||||
source: 'admin',
|
||||
payload: { action: 'extend_grace_period', extraDays, adminId, reason },
|
||||
});
|
||||
return { success: true };
|
||||
}
|
||||
async function adminCancelSubscription(subscriptionId, adminId, reason) {
|
||||
const sub = await repo.findById(subscriptionId);
|
||||
if (!sub)
|
||||
throw new errors_1.ValidationError('Subscription not found');
|
||||
await repo.setCancelled(sub.id, true);
|
||||
await repo.createEvent({
|
||||
subscriptionId,
|
||||
companyId: sub.companyId,
|
||||
eventType: 'subscription.canceled',
|
||||
source: 'admin',
|
||||
payload: { adminId, reason },
|
||||
});
|
||||
return { success: true };
|
||||
}
|
||||
async function adminReactivateSubscription(subscriptionId, adminId, reason) {
|
||||
const sub = await repo.findById(subscriptionId);
|
||||
if (!sub)
|
||||
throw new errors_1.ValidationError('Subscription not found');
|
||||
const periodEnd = addPeriod(new Date(), sub.billingPeriod);
|
||||
await repo.activateSubscription(subscriptionId, periodEnd);
|
||||
await repo.createEvent({
|
||||
subscriptionId,
|
||||
companyId: sub.companyId,
|
||||
eventType: 'subscription.reactivated',
|
||||
source: 'admin',
|
||||
payload: { adminId, reason, periodEnd },
|
||||
});
|
||||
return { success: true };
|
||||
}
|
||||
async function adminSuspendSubscription(subscriptionId, adminId, reason) {
|
||||
const sub = await repo.findById(subscriptionId);
|
||||
if (!sub)
|
||||
throw new errors_1.ValidationError('Subscription not found');
|
||||
await repo.setSuspended(sub.id);
|
||||
await repo.createEvent({
|
||||
subscriptionId,
|
||||
companyId: sub.companyId,
|
||||
eventType: 'subscription.suspended',
|
||||
source: 'admin',
|
||||
payload: { adminId, reason },
|
||||
});
|
||||
return { success: true };
|
||||
}
|
||||
//# sourceMappingURL=subscription.service.js.map
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user