/*********************************************************************************************************************************************************************** * 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, };