# Payment Module

Base contract for active payment modules.

Source: `src/core/modules/PaymentModule/PaymentModule.php`

For the complete pricing, manual-payment, order-persistence, and cart-rendering workflow, read [Payment Discounts And Cart Payments](../../../knowledge/21-payment-discounts.md) first.

## Class

| Item | Extends | Description |
|---|---|---|
| `PaymentModule` | none | Payment base class |

## Methods

| Method | Inputs | Description |
|---|---|---|
| `__construct` | `$paymentKey`, `$paymentModule` | Validates the config key against the provider module and prepares log separators |
| `getPaymentName` | none | Must return the provider module name |
| `getPaymentMethodKey` | none | Returns the configured key persisted with orders and transactions |
| `isProdMode` | none | Must return prod flag |
| `getLogDir` | `$type`, `$orderID` | Builds log file path |
| `saveLog` | `$type`, `$orderID`, `$log` | Appends payment log |
| `logPurchase` | `$accountModel`, `$paymentDetails` | Stores provider audit data and sends notification |
| `getURLHost` | none | Returns `WEB_URL` |
| `createRequest` | `$accountModel`, `$order`, `$newPrice` | Must start payment using the calculated final price; may return provider HTML |
| `checkPaymentStatus` | `$accountModel`, `$order` | Returns database payment state; providers may refresh unpaid transactions |
| `getUrlOk` | `$orderId` | Builds purchase-history return URL |

`PaymentModule::completePayment()` is the shared protected callback completion path. It recalculates the configured payment price, validates the received amount, writes the payment audit row, marks and fulfills the order, and sends the paid email for the first successful callback.

## Logs

| Item | Value |
|---|---|
| Base path | `ROOT.'../wwwLogs/payments/'.SERVER_NAME.'/'` |
| Path format | `<date>/<payment>/<type>/<orderID>.log` |
| Date folder | `Y-m-d` |
| Write mode | append |
| Dev output | printed when not prod |

## Payment Flow

1. The store form submits product, quantity, account, and payment method.
2. `StartPayment` sanitizes the method and reads `PAYMENT_MODULES[$method]`.
3. Disabled or missing methods return `false` to the store page.
4. Enabled methods with `payment_module = null` are manual payments and do not need a module file.
5. For provider methods, the controller loads `MODULES_FOLDER.<payment_module>/<payment_module>.php`.
6. The class name is built as `<Method>Payment` and the payment class is instantiated.
7. `createRequest($accountModel, $order, $newPrice)` starts the provider request with the final price.
8. The selected payment method is stored in `cms_orders.payment_method` before the provider request.
9. The provider redirects or calls back to the module webhook URL.
10. `PaymentHelpers::checkPaymentStatus()` resolves the stored provider and calls `checkPaymentStatus()`.
11. The base status method reports the database state; providers may query their API when the order is unpaid.
12. A manual URL payment keeps the order pending, returns the customer to the cart, and shows the configured URL and instructions to contact the GM.
13. A manual message payment keeps the order pending and renders its translated HTML message through the same payment-information block as provider checkout HTML.
14. The module validates the provider callback.
15. `PaymentModule::completePayment()` recalculates and validates the final price, then writes the payment audit row.
16. The shared completion path marks and fulfills the order idempotently.

Provider modules may return an HTML payment request from `createRequest()`. `PaymentHelpers::startPayment()` returns that HTML to the checkout controller, which renders it. Existing providers that print and exit, such as PayPal, retain their behavior.

Methods with `prod_mode = false` are hidden from non-admin users and rejected during customer checkout. Admins can use them. Webhook entrypoints and provider status refreshes are not subject to this customer-facing filter. Manual URL methods without `prod_mode` remain available.

## Dependencies

| Item | Use |
|---|---|
| `StoreModel::logPayment` | Payment audit row |
| `StoreModel::addPurchase` | Purchase fulfillment |
| `sendNotification` | Payment notification |
| `WEB_URL` | Return and webhook URLs |
| `SERVER_NAME` | Log folder scope |

Each entry in `PAYMENT_MODULES` may define `discount` as a numeric percentage. Positive values reduce the price and negative values increase it. The order stores the original total, calculated final total, and applied percentage; `cms_payments` stores provider gross values and the adjusted net money.

Entries may also define `additional_fee` as an optional percentage applied to the recorded `cms_payments.net_money` after the provider net amount is calculated. It is an internal estimate and does not change the customer charge, `mc_gross`, or `payment_gross`. Missing or `null` values are treated as `0`.

## Manual Payments

Manual payments are enabled entries with `payment_module = null`. They appear with provider payment methods and use the same percentage and final-price calculation. Selecting one creates the order normally, clears the cart, keeps the order `PENDING`, and returns the customer to the cart so the pending purchase is visible.

The cart shows an informational message linking to the configured URL and instructing the customer to complete the payment there and contact the GM. Example:

```php
'simple_url' => [
  'enabled' => true,
  'hide_for_manual_purchases' => true,
  'label' => 'Contact us',
  'discount' => 0,
  'url' => WHATSAPP_URL,
],
```

`enabled` controls customer visibility but show for admins and in admin panel

Message-based manual payments use a translation key instead of a URL:

```php
'bank_transfer' => [
  'enabled' => true,
  'payment_module' => null,
  'label' => 'Bank transfer',
  'discount' => 0,
  'url' => WHATSAPP_URL,
  'message' => 'payments.manual.bank_transfer',
  'receiver_data' => [
    'bankname' => '',
    'iban' => '',
    'receivername' => '',
    'additionalinformation' => '',
  ],
],
```

`message` selects the translated instruction template. `receiver_data` supplies the labeled `<ul>` properties rendered below that template. The configured `url` is used as the generic contact link. `enabled` controls customer visibility.

Message-based manual payments use a translation key instead of a URL:

```php
'bank_transfer' => [
  'enabled' => true,
  'payment_module' => null,
  'label' => 'Bank transfer',
  'discount' => 0,
  'url' => WHATSAPP_URL,
  'message' => 'payments.manual.bank_transfer',
  'receiver_data' => [
    'bankname' => '',
    'iban' => '',
    'receivername' => '',
    'additionalinformation' => '',
  ],
],
```

`message` selects the translated instruction template. `receiver_data` supplies the labeled `<ul>` properties rendered below that template. The configured `url` is used as the generic contact link. `enabled` controls customer visibility.

## New Methods

1. Add a `PAYMENT_MODULES` entry with `payment_module` set to the provider module, or `null` for manual methods.
2. Create `src/core/modules/<payment_module>/<payment_module>.php` for automatic providers.
3. Implement the provider class in `src/core/modules/<payment_module>/<payment_module>.class.php`.
4. Extend `PaymentModule`.
5. Return `<payment_module>` from `getPaymentName`; accept the selected config key in the provider constructor for persistence and provider configuration.
6. Implement `isProdMode` from config.
7. Implement `createRequest` for provider checkout.
8. Add a webhook action in `<method>.php`.
9. Validate account, product, amount, status, and transaction id.
10. Map provider callbacks into the shared `completePayment()` path.
11. Let the shared path write the audit row and fulfill only after final paid status.

## PayPal v2

See [PayPal v2 Module](paypalv2.module.md) for the complete configuration, SDK wallet flow, server validation boundary, webhook, retry, idempotency, and reconciliation reference.

## Admin Approval

`AdminApprovalPayment` is an internal payment module and is not configured in `PAYMENT_MODULES`. Its `createRequest()` method intentionally performs no external request. `validatePayment()` is called only by the GM-only Orders Management page, validates the approval form, and invokes the shared completion behavior with the payment method selected by the admin.

Admin approval does not call `OrderModel::isPayable()`. It can complete `PENDING` or `REJECTED` orders through the separate `OrderModel::markPaidByAdmin()` path. Existing customer checkout, PayPal validation, models, and views retain their current behavior.

## Refactor Notes

1. Keep provider libraries wrapped by module classes.
2. Keep webhook validation idempotent.
3. Use the order transaction ID before granting purchases.
4. Log rejected callbacks with enough data.
5. Keep `createRequest` responsible for external redirects and use its supplied `$newPrice`.

## Override

- `src/core/modules/<method>/<method>.php`
- `src/core/modules/<method>/<method>.class.php`

```php
class <Method>Payment extends PaymentModule
{
  public function getPaymentName() { return '<method>'; }
  public function isProdMode() { return <prodFlag>; }
  public function createRequest($accountModel, $order, $newPrice) { <providerRequest>; }
}
```
