refactor(billing): clean architecture with payment gateway abstraction
- Add PaymentGatewayInterface contract for all payment providers - Add PayPalGateway (reads credentials from config, not hardcoded) - Add ManualGateway for admin-triggered payments - Add StripeGateway stub for future implementation - Add GatewayFactory for gateway instantiation by name - Add BillingRepository: parameterized-SQL data layer - Add BillingService: pricing, invoice creation, payment processing - Add gsp_billing_transactions table (DB version 2) for audit trail - Add new columns to gsp_billing_invoices (home_id, rate_type, players, period_start/end, subtotal, total_due, payment_status) - Add gsp_billing_service_remote_servers mapping table - Move PayPal credentials from api files into config.inc.php - Fix double session_start() bug in capture_order.php - Replace raw SQL with prepared statements throughout - Refactor admin_invoices.php to use billing_invoices + BillingRepository - Refactor admin_payments.php to read from gsp_billing_transactions - Update admin.php with links to Transaction Log and Manage Invoices Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
This commit is contained in:
parent
b0e00c9370
commit
986a4e53b4
14 changed files with 1227 additions and 749 deletions
188
modules/billing/classes/BillingService.php
Normal file
188
modules/billing/classes/BillingService.php
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
<?php
|
||||
require_once __DIR__ . '/../classes/BillingRepository.php';
|
||||
require_once __DIR__ . '/../classes/PaymentGatewayInterface.php';
|
||||
|
||||
/**
|
||||
* BillingService — core business logic for the billing module.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Calculate pricing
|
||||
* - Create invoices
|
||||
* - Process payment results (log transaction, mark invoice paid, update server home)
|
||||
* - Extend / reset server billing expiration
|
||||
*/
|
||||
class BillingService
|
||||
{
|
||||
private BillingRepository $repo;
|
||||
|
||||
public function __construct(BillingRepository $repo)
|
||||
{
|
||||
$this->repo = $repo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate pricing for a new order.
|
||||
*
|
||||
* @param array $service Row from gsp_billing_services
|
||||
* @param string $rateType 'daily' | 'monthly' | 'yearly'
|
||||
* @param int $players Number of player slots
|
||||
* @param int $qty Duration quantity (e.g. 2 = 2 months)
|
||||
* @return array { rate_per_player, subtotal, total_due, period_days }
|
||||
*/
|
||||
public function calculatePrice(array $service, string $rateType, int $players, int $qty = 1): array
|
||||
{
|
||||
$qty = max(1, $qty);
|
||||
$players = max(1, $players);
|
||||
|
||||
switch ($rateType) {
|
||||
case 'daily':
|
||||
$basePrice = (float)($service['price_daily'] ?? 0);
|
||||
$periodDays = $qty;
|
||||
break;
|
||||
case 'yearly':
|
||||
$basePrice = (float)($service['price_year'] ?? 0);
|
||||
$periodDays = $qty * 365;
|
||||
break;
|
||||
case 'monthly':
|
||||
default:
|
||||
$rateType = 'monthly';
|
||||
$basePrice = (float)($service['price_monthly'] ?? 0);
|
||||
$periodDays = $qty * 31;
|
||||
break;
|
||||
}
|
||||
|
||||
// price_monthly etc is the per-player per-period rate
|
||||
$ratePerPlayer = $basePrice;
|
||||
$subtotal = round($ratePerPlayer * $players * $qty, 2);
|
||||
$totalDue = $subtotal;
|
||||
|
||||
return [
|
||||
'rate_type' => $rateType,
|
||||
'rate_per_player' => $ratePerPlayer,
|
||||
'players' => $players,
|
||||
'qty' => $qty,
|
||||
'subtotal' => $subtotal,
|
||||
'total_due' => $totalDue,
|
||||
'period_days' => $periodDays,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a billing invoice row.
|
||||
*
|
||||
* @param array $pricing Result from calculatePrice()
|
||||
* @param array $context { user_id, service_id, home_id, home_name, customer_name, customer_email, description }
|
||||
* @return int New invoice_id (0 on failure)
|
||||
*/
|
||||
public function createInvoice(array $pricing, array $context): int
|
||||
{
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$periodStart = $now;
|
||||
$periodEnd = date('Y-m-d H:i:s', strtotime('+' . $pricing['period_days'] . ' days'));
|
||||
|
||||
return $this->repo->createInvoice([
|
||||
'user_id' => intval($context['user_id'] ?? 0),
|
||||
'service_id' => intval($context['service_id'] ?? 0),
|
||||
'home_id' => intval($context['home_id'] ?? 0),
|
||||
'home_name' => $context['home_name'] ?? '',
|
||||
'customer_name' => $context['customer_name'] ?? '',
|
||||
'customer_email' => $context['customer_email'] ?? '',
|
||||
'rate_type' => $pricing['rate_type'],
|
||||
'rate_per_player' => $pricing['rate_per_player'],
|
||||
'players' => $pricing['players'],
|
||||
'period_start' => $periodStart,
|
||||
'period_end' => $periodEnd,
|
||||
'subtotal' => $pricing['subtotal'],
|
||||
'total_due' => $pricing['total_due'],
|
||||
'currency' => $context['currency'] ?? 'USD',
|
||||
'payment_status' => 'unpaid',
|
||||
'payment_method' => '',
|
||||
'description' => $context['description'] ?? '',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a successful payment result from a gateway.
|
||||
*
|
||||
* 1. Log the transaction
|
||||
* 2. Mark invoice paid
|
||||
* 3. Update server home billing state (extend or reset expiration)
|
||||
*
|
||||
* @param array $captureResult Result from PaymentGatewayInterface::handleCallback()
|
||||
* @param int $invoiceId
|
||||
* @param int $userId
|
||||
* @param int $homeId
|
||||
* @param array $invoiceRow The invoice row (from DB) — needed for period/pricing
|
||||
* @return array { success: bool, transaction_id: string, error?: string }
|
||||
*/
|
||||
public function processPaymentSuccess(
|
||||
array $captureResult,
|
||||
int $invoiceId,
|
||||
int $userId,
|
||||
int $homeId,
|
||||
array $invoiceRow
|
||||
): array {
|
||||
$txid = $captureResult['transaction_id'] ?? null;
|
||||
$method = $captureResult['payment_method'] ?? 'paypal';
|
||||
$amount = (float)($captureResult['amount'] ?? $invoiceRow['total_due'] ?? 0);
|
||||
$currency = $captureResult['currency'] ?? $invoiceRow['currency'] ?? 'USD';
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
// 1. Log transaction
|
||||
$this->repo->logTransaction([
|
||||
'invoice_id' => $invoiceId,
|
||||
'user_id' => $userId,
|
||||
'home_id' => $homeId,
|
||||
'payment_method' => $method,
|
||||
'transaction_external_id' => $txid ?? '',
|
||||
'amount' => $amount,
|
||||
'currency' => $currency,
|
||||
'status' => 'completed',
|
||||
'raw_response' => $captureResult['raw_response'] ?? [],
|
||||
]);
|
||||
|
||||
// 2. Mark invoice paid
|
||||
if ($invoiceId > 0) {
|
||||
$this->repo->markInvoicePaid($invoiceId, $txid ?? '', $method, $now);
|
||||
}
|
||||
|
||||
// 3. Update server home billing state
|
||||
if ($homeId > 0) {
|
||||
$this->extendServerBilling($homeId, $invoiceRow, $now);
|
||||
}
|
||||
|
||||
return ['success' => true, 'transaction_id' => $txid];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend or reset a server's billing expiration based on the invoice period.
|
||||
*/
|
||||
public function extendServerBilling(int $homeId, array $invoiceRow, string $now): void
|
||||
{
|
||||
$home = $this->repo->getServerHomeBilling($homeId);
|
||||
$periodEnd = $invoiceRow['period_end'] ?? null;
|
||||
|
||||
if (!$periodEnd) {
|
||||
$rateType = $invoiceRow['rate_type'] ?? 'monthly';
|
||||
$periodMap = ['daily' => '+1 day', 'monthly' => '+31 days', 'yearly' => '+365 days'];
|
||||
$periodEnd = date('Y-m-d H:i:s', strtotime($periodMap[$rateType] ?? '+31 days'));
|
||||
}
|
||||
|
||||
// If current expiry is in the future, extend from it; otherwise reset from period_end
|
||||
$currentExpiry = $home['billing_expires_at'] ?? null;
|
||||
if ($currentExpiry && strtotime($currentExpiry) > time()) {
|
||||
$currentPeriodSecs = strtotime($invoiceRow['period_end'] ?? $now) - strtotime($invoiceRow['period_start'] ?? $now);
|
||||
$newExpiry = date('Y-m-d H:i:s', strtotime($currentExpiry) + max(86400, $currentPeriodSecs));
|
||||
} else {
|
||||
$newExpiry = $periodEnd;
|
||||
}
|
||||
|
||||
$this->repo->updateServerHomeBilling($homeId, [
|
||||
'billing_status' => 'active',
|
||||
'billing_expires_at' => $newExpiry,
|
||||
'next_invoice_date' => $newExpiry,
|
||||
'server_expiration_date' => null,
|
||||
'billing_invoice_sent_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue