mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
121 lines
5.0 KiB
JavaScript
121 lines
5.0 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
|
|
if (rule.max_uses != null) {
|
|
const uses = await mdl_Payments.count({
|
|
where: { promo_code: code, plan_id: plan.plan_id },
|
|
});
|
|
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,
|
|
};
|