archetecture security fix

This commit is contained in:
root
2026-06-11 03:22:12 -04:00
parent 6def9993da
commit 9483750161
3126 changed files with 177194 additions and 37211 deletions
@@ -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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"}
+301
View File
@@ -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