Files
starr-philproperties/apps/api/services/payment.service.js
T
2026-08-31 17:57:29 +08:00

124 lines
5.2 KiB
JavaScript

/***********************************************************************************************************************************************************************
* File Name: payment.service.js
* Type of Program: Service
* Description: Provider-agnostic payment orchestration.
* - Loads payment policy per plan (promo rules, refund policy, allowed providers)
* - Evaluates promo codes server-side (type: flat | percent)
* - Calculates refund eligibility window (unit: minutes | hours | days)
* - Delegates create/capture/refund to the provider registry
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 29, 2026
***********************************************************************************************************************************************************************/
'use strict';
const registry = require('../providers/registry');
const mdl_PaymentPolicies = require('../models/tiers/payment_policies.mdl');
const mdl_Payments = require('../models/tiers/payments.mdl');
// ─── Defaults ─────────────────────────────────────────────────────────────────
const DEFAULT_REFUND_POLICY = {
allowed: true,
window_value: 5,
window_unit: 'minutes',
reason_required: false,
};
const UNIT_MS = { minutes: 60_000, hours: 3_600_000, days: 86_400_000 };
// ─── Policy ───────────────────────────────────────────────────────────────────
async function getPolicyForPlan(plan_id) {
return mdl_PaymentPolicies.findOne({ where: { plan_id } });
}
// ─── Refund window ────────────────────────────────────────────────────────────
function getRefundWindowMs(policy) {
const rp = policy?.refund_policy ?? DEFAULT_REFUND_POLICY;
const { window_value = 5, window_unit = 'minutes' } = rp;
return Number(window_value) * (UNIT_MS[window_unit] ?? 60_000);
}
function isRefundAllowed(policy) {
return policy?.refund_policy?.allowed ?? DEFAULT_REFUND_POLICY.allowed;
}
// ─── Promo evaluation ─────────────────────────────────────────────────────────
// effectivePrice — pass the localized price when charging in a non-base currency
// so discounts are computed against the actual amount being charged.
async function evaluatePromo(policy, plan, rawCode, effectivePrice = null) {
const code = rawCode?.trim?.().toUpperCase?.() ?? null;
if (!code) return { valid: false, reason: 'No promo code provided.' };
const rules = policy?.promo_rules ?? [];
const rule = rules.find((r) => r.code?.toUpperCase() === code);
if (!rule) return { valid: false, reason: 'Invalid promo code.' };
if (rule.expires_at && new Date(rule.expires_at) < new Date())
return { valid: false, reason: 'Promo code has expired.' };
// Count how many completed payments used this code for this plan. Scoped to
// 'completed' only — pending/failed/cancelled attempts must NOT consume a
// limited-use code, or anyone can exhaust max_uses by creating orders they
// never pay for.
if (rule.max_uses != null) {
const uses = await mdl_Payments.count({
where: { promo_code: code, plan_id: plan.plan_id, status: 'completed' },
});
if (uses >= Number(rule.max_uses))
return { valid: false, reason: 'Promo code has reached its usage limit.' };
}
const subtotal = effectivePrice !== null ? Number(effectivePrice) : Number(plan.price);
if (rule.min_amount != null && subtotal < Number(rule.min_amount))
return { valid: false, reason: `This promo code requires a minimum purchase of ${rule.min_amount}.` };
let discount;
if (rule.type === 'flat') {
discount = Math.min(Number(rule.value), subtotal);
} else if (rule.type === 'percent') {
const pct = Math.min(Number(rule.value), 100);
const raw = (subtotal * pct) / 100;
discount = rule.max_discount != null ? Math.min(raw, Number(rule.max_discount)) : raw;
discount = Math.min(discount, subtotal);
} else {
return { valid: false, reason: 'Unsupported promo type.' };
}
return {
valid: true,
code,
type: rule.type,
value: rule.value,
discount: Number(discount.toFixed(2)),
reason: null,
};
}
// ─── Provider delegation ──────────────────────────────────────────────────────
function createOrder(provider, opts) {
return registry.get(provider).createOrder(opts);
}
function captureOrder(provider, orderId) {
return registry.get(provider).captureOrder(orderId);
}
function refundCapture(provider, captureId, amount, currency) {
return registry.get(provider).refundCapture(captureId, amount, currency);
}
module.exports = {
getPolicyForPlan,
getRefundWindowMs,
isRefundAllowed,
evaluatePromo,
createOrder,
captureOrder,
refundCapture,
};