Appearance
Payment Out Complete Walkthrough — Example-Based Documentation
Overview
This document demonstrates the complete lifecycle of a Payment Out transaction using real payload examples, showing exactly which tables and columns are affected during Add (Draft), Post, and Unpost phases.
A Payment Out records money sent from a bank account or treasury to a vendor (or other payee). It can optionally:
- Include a payment method commission (e.g. bank transfer fee, cheque commission) with optional VAT on that commission
- Be linked to a source document (e.g. a POS session or fund transfer)
- Be issued via cheque with a future due date
Unlike Payment In, Payment Out does not auto-reconcile purchase invoices at creation time — reconciliation is handled separately through the Purchase Invoice Reconciliation flow.
Example Payload — Vendor Payment (Bank Transfer)
json
{
"paymentOutDate": "2026-01-28T17:00:00",
"description": "Payment to supplier for PI-2026-0001",
"currencyId": 4,
"rate": 1.0,
"paymentHub": "Bank",
"paymentHubDetailId": 12,
"glAccountId": null,
"paymentOutDetails": [
{
"paidBy": "Vendor",
"paidByDetailsId": 201,
"paymentMethodId": 5,
"amount": 1000.00,
"currencyId": 4,
"rate": 1.0,
"costCenters": [],
"methodDetails": {
"chequeNumber": null,
"chequeDueDate": null,
"bankReference": "TRF-20260128-002",
"commissionType": null,
"commissionValue": 0,
"allowVat": false,
"vatRatio": 0
}
}
]
}Pre-Save Calculations
Detail-Level:
LocalAmount = Amount × Rate = 1000 × 1.0 = 1000
CommissionAmount = 0 (no commission)
VatAmount = 0 (no VAT on commission)
NetAmount = Amount + CommissionAmount + VatAmount = 1000 + 0 + 0 = 1000
NetLocalAmount = NetAmount × Rate = 1000 × 1.0 = 1000
Note: For Payment Out, NetAmount = Amount + Commission + VAT
(commission is an additional cost on top of what the vendor receives)
Header Totals:
Amount = Sum of detail amounts = 1000
LocalAmount = Sum of detail local amounts = 1000
NetAmount = Sum of detail net amounts = 1000
NetLocalAmount = Sum of detail net local amounts = 1000Phase 1: Add Payment Out (Draft)
Handler: AddPaymentOutCommandHandler
Action: Creates a draft payment-out record with no GL or bank impact.
Validations Performed
| Check | Rule | Result |
|---|---|---|
| Payment date in open period | Resolved via GetCurrentPeriodQuery(PaymentOutDate) | ✅ Jan 2026 open |
| Currency required | CurrencyId must be provided | ✅ SAR (4) provided |
| Rate required | Rate > 0 | ✅ 1.0 provided |
| Payment hub | Must be Bank or Treasury | ✅ Bank |
| PaymentHubDetailId required | Must be provided — resolves to BankAccountId (hub=Bank) or TreasuryId (hub=Treasury) | ✅ 12 provided |
| Details not empty | At least one detail line | ✅ 1 detail |
| Detail paid-by | PaidBy must be Customer, Vendor, or Other | ✅ Vendor |
| Paid-by entity | PaidByDetailsId must reference valid vendor | ✅ Vendor 201 |
| Payment method | Must reference valid PaymentMethod | ✅ Method 5 (Transfer) |
| Amount > 0 | Each detail amount must be positive | ✅ 1000 > 0 |
| Bank balance sufficient | Bank/treasury current balance ≥ total amount | ✅ Balance OK |
| Cheque fields | If PaymentMethodType = Check, ChequeNumber + ChequeDueDate required | ✅ Not cheque |
| Cost center sum | If account requires cost centers, percentages must sum to 100% | ✅ No cost centers required |
Table 1: PaymentOutHeader (INSERT)
Operation: INSERT new record
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | e.g., b2c3d4e5-2345-6789-bcde-f01234567890 |
| Code | "PO-2026-0001" | Sequence Service | Generated by UpdateLastSequenceCommand |
| PaymentOutDate | 2026-01-28 17:00:00 | Payload | Transaction date |
| Description | "Payment to supplier for PI-2026-0001" | Payload | Optional notes |
| Status | "Draft" | SetAsDraft() | TransactionStatus enum |
| PaymentHub | "Bank" | Payload | PaymentPlace enum: Bank or Treasury |
| PaymentHubDetailId | 12 | Payload | Holds BankAccountId when PaymentHub = Bank; holds TreasuryId when PaymentHub = Treasury |
| CurrencyId | 4 | Payload | Saudi Riyal |
| Rate | 1.0 | Payload | Exchange rate to base currency |
| Amount | 1000.00 | Calculated | Sum of detail amounts |
| LocalAmount | 1000.00 | Amount × Rate | Base currency amount |
| NetAmount | 1000.00 | Amount + Commission + VAT | Total disbursed (including fees) |
| NetLocalAmount | 1000.00 | NetAmount × Rate | Base currency net |
| GlAccountId | NULL | Payload | Override GL account for header (optional) |
| FinancialYearPeriodId | [Integer DB ID] | GetCurrentPeriodQuery(PaymentOutDate) | Resolved from date — payload string is for display only |
| JournalId | NULL | Draft state | Set on post |
| JournalCode | NULL | Draft state | Set on post |
| SourceDocumentType | NULL | Not linked | Set for POS / system-generated payments |
| SourceDocumentId | NULL | Not linked | Reference to source transaction |
| SourceDocumentCode | NULL | Not linked | Source transaction code |
| UnpostedReason | NULL | Not yet unposted | Reason text stored on unpost |
| UnpostedBy | NULL | Not yet unposted | User who unposted |
| UnpostedOn | NULL | Not yet unposted | Timestamp of unpost |
| WorkflowStatus | NULL | No workflow yet | RequestStatus enum |
| POSSessionId | NULL | Not POS | POS session reference |
| BranchId | [Current Branch] | ISecurityService | Branch context |
| CompanyId | [Current Company] | ISecurityService | Company context |
| CreatedDate | [Current Timestamp] | Audit | Auto-set |
| CreatedBy | [Current User ID] | ISecurityService | Authenticated user |
| TenantId | [Tenant GUID] | ISecurityService | Multi-tenancy isolation |
| IsDeleted | false | Default | Soft delete flag |
Table 2: PaymentOutDetail (INSERT)
Operation: INSERT one record per detail line
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | Detail line ID |
| PaymentOutId | [Header GUID] | FK | Parent payment-out |
| PaidBy | "Vendor" | Payload | PaidBy enum: Customer / Vendor / Other |
| PaidByDetailsId | 201 | Payload | Vendor ID (FK to Vendor table) |
| VendorName | "ABC Supplier" | Resolved | Vendor display name |
| VendorNameAr | "مورد ABC" | Resolved | Arabic vendor name |
| CustomerId | NULL | Not customer | Set when PaidBy = Customer |
| GlAccountId | NULL | Optional | Override GL account for this line |
| PaymentMethodId | 5 | Payload | References PaymentMethod table |
| Amount | 1000.00 | Payload | In foreign currency |
| LocalAmount | 1000.00 | Amount × Rate | Base currency |
| NetAmount | 1000.00 | Amount + Commission + VAT | Total leaving bank for this line |
| NetLocalAmount | 1000.00 | NetAmount × Rate | Base currency net |
| Rate | 1.0 | Payload | Exchange rate |
| CurrencyId | 4 | Payload | Saudi Riyal |
| Notes | NULL | Payload | Optional line notes |
| IsVatLine | false | Payload | Not a VAT line |
| HasVat | false | Payload | No VAT on this line |
| EntityBalance | NULL | Resolved | Vendor's current outstanding balance |
| CreatedDate | [Current Timestamp] | Audit | Auto-set |
| CreatedBy | [Current User ID] | ISecurityService | Authenticated user |
| TenantId | [Tenant GUID] | ISecurityService | Multi-tenancy |
Table 3: PaymentOutMethodDetail (INSERT)
Operation: INSERT one record per detail (stores payment method specifics)
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | Method detail ID |
| PaymentOutDetailId | [Detail GUID] | FK | Parent detail line |
| PaymentMethodId | 5 | Payload | PaymentMethod reference |
| ChequeNumber | NULL | Not cheque | Populated for Check type |
| ChequeDueDate | NULL | Not cheque | Populated for Check type |
| BankReference | "TRF-20260128-002" | Payload | Bank transfer reference |
| CommissionAmount | 0 | Calculated | 0 — no commission configured |
| VatAmount | 0 | Calculated | 0 — AllowVat = false |
| CommissionType | NULL | No commission | Amount or Percent |
| CommissionValue | 0 | Payload | Commission percentage or fixed amount |
| CommissionAccountId | NULL | No commission | GL account for commission expense |
| AllowVat | false | Payload | VAT on commission disabled |
| VatRatio | 0 | Payload | 0% VAT on commission |
| VatAccountId | NULL | No VAT | GL account for commission VAT |
| CreatedDate | [Current Timestamp] | Audit | Auto-set |
Table 4: PaymentOutDetailCostCenter (INSERT)
Operation: NONE — no cost centers in this example
When cost centers are required (account's CostCenterConfig = Mandatory):
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | Allocation ID |
| PaymentOutDetailId | [Detail GUID] | FK | Parent detail |
| CostCenterId | [e.g. 87] | Payload | Cost center reference |
| Percentage | 100.0 | Payload | Allocation percentage |
| Amount | 1000.00 | Detail.NetLocalAmount × Percentage/100 | Allocated amount |
Summary of Add Phase (Draft)
Tables Affected:
- ✅ PaymentOutHeader — 1 INSERT
- ✅ PaymentOutDetail — 1 INSERT
- ✅ PaymentOutMethodDetail — 1 INSERT
- ❌ PaymentOutDetailCostCenter — 0 (no cost centers in this example)
- ❌ GL / Bank / Treasury — NO impact (draft only)
Phase 2: Post Payment Out
Handler: PostPaymentOutCommandHandler
Action: Finalizes the payment, creates a Journal Entry, records a Bank Transaction, and updates the vendor's financial position.
Prerequisites Check
| Check | Rule |
|---|---|
| Status = Draft | Cannot post already-posted payment |
| GL account for vendor | Vendor must have Payable Account configured |
| GL account for payment method | PaymentMethod must have a GL account (bank clearing or cash account) |
| GL account for commission (if any) | CommissionAccountId must be set |
| GL account for commission VAT (if any) | VatAccountId must be set when AllowVat = true |
| Open financial period | PaymentOutDate period must be open |
| Sufficient balance | Bank/treasury current balance ≥ NetAmount (total including commission) |
| Accounting module access | User must have access to create journal entries |
Table 1: PaymentOutHeader (UPDATE)
Operation: UPDATE existing record
| Column Name | Before (Draft) | After (Posted) | Notes |
|---|---|---|---|
| Status | "Draft" | "Posted" | TransactionStatus enum |
| JournalId | NULL | [Journal GUID] | Linked journal entry |
| JournalCode | NULL | "JE-2026-0130" | Journal entry code |
| UpdatedDate | NULL | [Current Timestamp] | Last update timestamp |
| UpdatedBy | NULL | [Current User ID] | User who posted |
Table 2: JournalEntry (INSERT — PaymentOut Journal)
Operation: INSERT via IAccountingPublicApi.AddJournal()
Type: PaymentOut
| Column Name | Value | Notes |
|---|---|---|
| Id | [Auto-Generated GUID] | Journal ID |
| Code | "JE-2026-0130" | Sequence code |
| JournalDate | 2026-01-28 17:00:00 | From PaymentOutDate |
| Type | "PaymentOut" | JournalEntryType enum |
| SourceDocument | "PaymentOut" | SourceDocument enum |
| SourceDocumentId | [PaymentOutHeader GUID] | Link to payment |
| SourceDocumentCode | "PO-2026-0001" | Payment code |
| Status | "Posted" | Immediately posted |
| TotalDebit | 1000.00 | Sum of debit lines |
| TotalCredit | 1000.00 | Sum of credit lines (balanced) |
| FinancialYearPeriodId | [Period ID] | From payment period |
| Description | "Payment to supplier for PI-2026-0001" | From payment description |
Table 3: JournalEntryLine (INSERT × 2 — PaymentOut Journal)
Line 1: Accounts Payable (Debit)
Vendor's outstanding balance reduced:
| Column | Value | Notes |
|---|---|---|
| AccountId | [Vendor.PayableAccountId] | AP account for vendor 201 |
| AccountCode | "2010" | e.g. Accounts Payable |
| DebitAmount | 1000.00 | Clears vendor balance |
| CreditAmount | 0 | No credit |
| DebitAmountLocal | 1000.00 | × Rate (1.0) |
| CurrencyId | 4 | SAR |
| CurrencyRate | 1.0 | Base currency |
| CostCenterId | NULL | No cost center for AP |
Line 2: Bank Account (Credit)
Money leaves the bank:
| Column | Value | Notes |
|---|---|---|
| AccountId | [BankAccount.GLAccountId] | GL account of bank account 12 |
| AccountCode | "1020" | e.g. Current Bank Account |
| DebitAmount | 0 | No debit |
| CreditAmount | 1000.00 | Full amount leaving bank |
| CreditAmountLocal | 1000.00 | × Rate (1.0) |
| CurrencyId | 4 | SAR |
| CurrencyRate | 1.0 | Base currency |
| CostCenterId | NULL | No cost center for bank |
Journal JE-2026-0130 — Payment Out
DR 2010 Accounts Payable (Vendor) 1,000.00
CR 1020 Bank Account 1,000.00
--------- ---------
Total 1,000.00 1,000.00Table 4: BankTransactionHeader (INSERT)
Operation: INSERT — records this payment in the bank's transaction ledger
| Column Name | Value | Notes |
|---|---|---|
| Id | [Auto-Generated GUID] | Transaction ID |
| BankAccountId | 12 | Sending bank account |
| TransactionDate | 2026-01-28 | From PaymentOutDate |
| TransactionType | "Out" | Money leaving the bank |
| Amount | 1000.00 | Payment amount |
| LocalAmount | 1000.00 | Base currency |
| SourceDocumentType | "PaymentOut" | Reference type |
| SourceDocumentId | [PaymentOutHeader GUID] | Link to payment |
| SourceDocumentCode | "PO-2026-0001" | Payment code |
| JournalId | [Journal GUID] | Link to journal |
| Status | "Posted" | Always posted |
| CreatedDate | [Current Timestamp] | Audit |
Table 5: BankTransactionDetail (INSERT)
Operation: INSERT — one per payment detail line
| Column Name | Value | Notes |
|---|---|---|
| BankTransactionHeaderId | [Header GUID] | Parent transaction |
| PaymentOutDetailId | [Detail GUID] | Link to payment detail |
| Amount | 1000.00 | Detail amount |
| LocalAmount | 1000.00 | Base currency |
| PaidBy | "Vendor" | Payee type |
| PaidByDetailsId | 201 | Vendor ID |
Summary of Post Phase
Tables Affected:
- ✅ PaymentOutHeader — 1 UPDATE (status, journal IDs)
- ✅ JournalEntry — 1 INSERT (PaymentOut journal, immediately Posted)
- ✅ JournalEntryLine — 2 INSERTs (AP DR + Bank CR)
- ✅ BankTransactionHeader — 1 INSERT
- ✅ BankTransactionDetail — 1 INSERT
GL Impact:
Account 2010 (Accounts Payable) DR 1,000.00
Account 1020 (Bank Account) CR 1,000.00Phase 3: Unpost Payment Out
Handler: UnpostPaymentOutCommandHandler
Action: Fully reverses a posted payment — removes the journal and bank transaction, returns to Draft. Requires a reason to be provided.
Prerequisites Check
| Check | Rule |
|---|---|
| Status = Posted | Cannot unpost a Draft payment |
| Reason provided | UnpostPaymentOutCommand.Reason must not be empty |
| Open financial period | PaymentOutDate period must still be open |
| Journal reversible | The linked journal must not already be reversed |
Tables Affected
| Table | Operation | Notes |
|---|---|---|
| PaymentOutHeader | UPDATE | Status → Draft, JournalId/JournalCode → NULL, UnpostedReason/UnpostedBy/UnpostedOn set |
| JournalEntry | Reversed | Journal reversed via ReversePaymentOutJournalAsync |
| BankTransactionHeader | Soft DELETE | Bank transaction removed |
| BankTransactionDetail | Soft DELETE | Bank detail removed |
Table: PaymentOutHeader (UPDATE on Unpost)
| Column | Before (Posted) | After (Unposted) | Notes |
|---|---|---|---|
| Status | "Posted" | "Draft" | Reset to Draft |
| JournalId | [GUID] | NULL | Cleared |
| JournalCode | "JE-2026-0130" | NULL | Cleared |
| UnpostedReason | NULL | "Correction needed" | Reason stored permanently |
| UnpostedBy | NULL | [Current User ID] | Who unposted |
| UnpostedOn | NULL | [Current Timestamp] | When unposted |
Note: Unlike other draft resets, the unpost reason fields are permanently stored for audit trail purposes even after the payment is re-posted.
Payment Out with Commission (Cheque with Bank Fee)
When a payment method has a commission configured (e.g. 2% cheque processing fee + 15% VAT), the journal gets additional lines and more money leaves the bank than what the vendor receives.
Example
json
{
"paymentOutDetails": [
{
"paidBy": "Vendor",
"paidByDetailsId": 201,
"paymentMethodId": 3,
"amount": 1000.00,
"methodDetails": {
"chequeNumber": "CHQ-002",
"chequeDueDate": "2026-02-15",
"commissionType": "Percent",
"commissionValue": 2.0,
"commissionAccountId": 6010,
"allowVat": true,
"vatRatio": 15.0,
"vatAccountId": 2030
}
}
]
}Commission Calculations
CommissionAmount = Amount × CommissionValue / 100 = 1000 × 2% = 20.00
VatAmount = CommissionAmount × VatRatio / 100 = 20 × 15% = 3.00
NetAmount = Amount + CommissionAmount + VatAmount = 1000 + 20 + 3 = 1023.00
Vendor receives: 1000.00 SAR
Bank deducts: 1023.00 SAR (1000 vendor + 20 commission + 3 VAT)Updated PaymentOutDetail
| Column | Value |
|---|---|
| Amount | 1000.00 |
| NetAmount | 1023.00 |
| LocalAmount | 1000.00 |
| NetLocalAmount | 1023.00 |
Journal Entry Lines (4 lines instead of 2)
JE-2026-0131 — Payment Out (with Commission)
DR 2010 Accounts Payable 1,000.00 ← clears vendor balance
DR 6010 Commission Expense 20.00 ← bank cheque commission
DR 2030 VAT Input (Commission) 3.00 ← VAT on commission
CR 1020 Bank Account 1,023.00 ← total leaving bank
--------- ---------
Total 1,023.00 1,023.00BankTransactionHeader Amount
The bank transaction records the full NetAmount (1023.00), not just the vendor amount:
| Column | Value |
|---|---|
| Amount | 1023.00 |
| LocalAmount | 1023.00 |
Payment Out via Treasury (Cash)
When PaymentHub = Treasury, the flow is identical to Bank but creates a TreasuryTransaction instead of a BankTransaction.
Key Differences
| Property | Bank | Treasury |
|---|---|---|
PaymentHub | Bank | Treasury |
PaymentHubDetailId | BankAccountId | TreasuryId |
| Balance check | Bank account balance | Treasury current balance |
| Transaction created | BankTransactionHeader/Detail | TreasuryTransactionHeader/Detail |
| GL credit account | BankAccount.GLAccountId | Treasury.GLAccountId |
Journal Entry Lines (Treasury Cash)
JE-2026-0132 — Cash Payment Out (Treasury)
DR 2010 Accounts Payable 1,000.00
CR 1001 Petty Cash / Treasury 1,000.00Paying a Customer (Refund / Debit Note)
When PaidBy = Customer, money is paid out to a customer (e.g. a refund or rebate).
The GL credit line uses the Customer's Receivable Account instead of a Vendor Payable Account:
JE-2026-0133 — Customer Refund Payment Out
DR 1010 Accounts Receivable (Customer) 500.00 ← reduces customer balance
CR 1020 Bank Account 500.00When PaidBy = Other, the GL account is taken from the detail's GlAccountId field (a manually specified account).
Paying with Cheque (Post-Dated)
Cheques have a ChequeDueDate that may be in the future. The journal is created at posting time (today), not at cheque due date.
json
"methodDetails": {
"chequeNumber": "CHQ-003",
"chequeDueDate": "2026-03-01",
"bankReference": null
}The cheque details are stored in PaymentOutMethodDetail for reference. The bank transaction and journal are created immediately when posted — the ERP does not automatically defer posting to the cheque due date.
Multi-Currency Payment Out
When paying a vendor in a foreign currency (e.g. USD), amounts are stored in both currencies.
Example — USD Payment at 3.75 SAR/USD
json
{
"currencyId": 1,
"rate": 3.75,
"paymentOutDetails": [
{
"amount": 1000.00,
"currencyId": 1,
"rate": 3.75
}
]
}Stored Values
| Column | Header | Detail |
|---|---|---|
| Amount | 1000.00 USD | 1000.00 USD |
| LocalAmount | 3750.00 SAR | 3750.00 SAR |
| NetAmount | 1000.00 USD | 1000.00 USD |
| NetLocalAmount | 3750.00 SAR | 3750.00 SAR |
Journal Entry Lines
JE-2026-0134 — Payment Out (USD)
DR 2010 Accounts Payable 3,750.00 SAR (= 1000 USD × 3.75)
CR 1020 Bank Account 3,750.00 SARAll GL postings use local (base) currency amounts. Foreign amounts are stored for reference only.
Complete Transaction Flow Summary
1. User submits Add Payment Out request
↓
2. AddPaymentOutCommandHandler.Handle()
↓
3. Validate: payment date, hub, currency, vendor, payment method,
commission fields, cheque fields, cost center sums
↓
4. GetCurrentPeriodQuery(PaymentOutDate) → FinancialYearPeriodId
↓
5. Load payment methods with commission data
↓
6. Calculate detail amounts:
- LocalAmount = 1000 × 1.0 = 1000
- CommissionAmount = 0
- VatAmount = 0
- NetAmount = 1000
- NetLocalAmount = 1000
↓
7. Validate bank/treasury balance ≥ NetAmount
↓
8. Generate sequence code → "PO-2026-0001"
↓
9. INSERT PaymentOutHeader (Status = Draft)
INSERT PaymentOutDetail × 1
INSERT PaymentOutMethodDetail × 1
↓
10. SaveChanges → committed
↓
11. Return PaymentOutHeaderId
--- DRAFT PHASE COMPLETE ---
12. User clicks "Post"
↓
13. PostPaymentOutCommandHandler.Handle()
↓
14. ValidatePreConditions():
Draft status, Vendor GL account set, open period, balance sufficient
↓
15. CanPostJournal(): User has Accounting module access
↓
16. BuildJournalCommand():
Header line: CR Bank/Treasury GL 1000 (money leaves bank)
Detail line: DR Vendor AP GL 1000 (clears payable)
(no commission lines — commission = 0)
↓
17. Call IAccountingPublicApi.AddJournal() → JournalCreatedDto
↓
18. InsertIntoEntityTransactions():
INSERT BankTransactionHeader (Type = Out, Amount = 1000)
INSERT BankTransactionDetail × 1
↓
19. UPDATE PaymentOutHeader:
Status = Posted
JournalId = [GUID]
JournalCode = "JE-2026-0130"
↓
20. SaveChanges → committed
↓
21. Return success
--- POST PHASE COMPLETE ---
22. (If correction needed) User clicks "Unpost" + provides reason
↓
23. UnpostPaymentOutCommandHandler.Handle()
↓
24. Validate: Posted, reason not empty, open period, journal reversible
↓
25. Call IAccountingPublicApi.ReversePaymentOutJournalAsync()
→ Reverses JournalEntry (swaps DR/CR lines)
↓
26. SoftDeleteLiquidityTransactions():
Soft DELETE BankTransactionHeader
Soft DELETE BankTransactionDetail
↓
27. PaymentOutHeader.Unpost():
Status = Draft
JournalId = NULL
JournalCode = NULL
UnpostedReason = "Correction needed"
UnpostedBy = [Current User ID]
UnpostedOn = [Current Timestamp]
↓
28. SaveChanges → committed
--- UNPOST PHASE COMPLETE ---Database Transactions
Add Phase
sql
BEGIN TRANSACTION
INSERT INTO PaymentOutHeader VALUES (...) -- Status = Draft
INSERT INTO PaymentOutDetail VALUES (...) -- 1 detail
INSERT INTO PaymentOutMethodDetail VALUES (...) -- Method detail
COMMIT TRANSACTIONDuration: ~40–80ms
Post Phase
sql
BEGIN TRANSACTION
-- Step 1: Create journal (via Accounting public API)
INSERT INTO JournalEntry VALUES (...) -- Type = PaymentOut, Posted
INSERT INTO JournalEntryLine VALUES (...) -- 2 lines (AP DR + Bank CR)
-- Step 2: Create bank transaction
INSERT INTO BankTransactionHeader VALUES (...)
INSERT INTO BankTransactionDetail VALUES (...)
-- Step 3: Update payment header
UPDATE PaymentOutHeader
SET Status = 'Posted',
JournalId = @JournalId,
JournalCode = @JournalCode,
UpdatedDate = GETUTCDATE(),
UpdatedBy = @UserId
WHERE Id = @PaymentOutId
COMMIT TRANSACTIONDuration: ~100–300ms
Unpost Phase
sql
BEGIN TRANSACTION
-- Step 1: Reverse journal (via Accounting public API)
-- Creates reversed journal entry with swapped DR/CR
-- Step 2: Soft delete bank transaction
UPDATE BankTransactionHeader SET IsDeleted = 1 WHERE SourceDocumentId = @PaymentOutId
UPDATE BankTransactionDetail SET IsDeleted = 1 WHERE ...
-- Step 3: Reset payment header
UPDATE PaymentOutHeader
SET Status = 'Draft',
JournalId = NULL,
JournalCode = NULL,
UnpostedReason = @Reason,
UnpostedBy = @UserId,
UnpostedOn = GETUTCDATE()
WHERE Id = @PaymentOutId
COMMIT TRANSACTIONDuration: ~100–250ms
Error Scenarios
Add / Edit Validation Failures
| Error Condition | Error Message | Resolution |
|---|---|---|
| Closed financial period | "Payment date must be in an open financial period" | Change date or open period |
| Missing PaymentHubDetailId | "PaymentHubDetailId is required" | Provide the BankAccountId (hub=Bank) or TreasuryId (hub=Treasury) |
| Missing cheque data | "Cheque number and due date are required for cheque payments" | Fill cheque fields |
| Insufficient bank balance | "Insufficient balance in bank account" | Reduce amount or use different account |
| Cost center sum ≠ 100% | "Cost center allocations must total 100%" | Adjust percentages |
| Amount ≤ 0 | "Payment amount must be greater than zero" | Correct amount |
| Details empty | "At least one payment detail is required" | Add a detail line |
Post Validation Failures
| Error Condition | Error Message | Resolution |
|---|---|---|
| Not Draft | "Payment must be in Draft status to post" | Already posted |
| Missing vendor GL account | "Vendor payable account not configured" | Configure vendor accounting |
| Missing payment method GL | "Payment method GL account not configured" | Configure method account |
| Missing commission GL | "Commission account required for payment method with commission" | Set CommissionAccountId |
| Insufficient balance at post time | "Insufficient balance to complete payment" | Top up bank account |
| No Accounting module access | "User does not have access to create journal entries" | Check user permissions |
| Closed period | "Cannot post in a closed financial period" | Reopen period |
Unpost Validation Failures
| Error Condition | Error Message | Resolution |
|---|---|---|
| Not Posted | "Payment must be Posted to unpost" | Nothing to unpost |
| Missing reason | "Reason is required to unpost" | Provide UnpostReason |
| Closed period | "Payment period is closed — cannot unpost" | Reopen period |
| Journal already reversed | "Journal entry has already been reversed" | Cannot reverse twice |
Payment Method Types Reference
| Type | CommissionData | ChequeFields | Notes |
|---|---|---|---|
| Cash | Optional | No | Physical cash disbursed |
| Check | Optional | Required (ChequeNumber + ChequeDueDate) | Post-dated cheques; journal created at post time |
| Visa | Optional | No | Card payment, commission = merchant fee |
| MasterCard | Optional | No | Card payment |
| SPAN / Mada | Optional | No | Local debit network |
| Transfer | Optional | No | Bank wire transfer; BankReference used |
Key Interfaces Used
| Interface | Usage |
|---|---|
IAppsUnitOfWork | Repository access for all ERP entities |
ISecurityService | Current user, tenant, branch, company context |
IClockService | Testable timestamp provider |
IGeneralSettingsPublicApi | Sequence generation, branch/company context, financial period |
IAccountingPublicApi | Creates and reverses Journal Entries |
IPaymentOutProcessingService | Encapsulates payment creation, posting, journal building, commission logic, balance validation |
Payment In vs Payment Out — Side-by-Side
| Property | Payment In | Payment Out |
|---|---|---|
| Direction | Money received | Money sent |
| Typical counterparty | Customer | Vendor |
| Bank line | DR Bank | CR Bank |
| AR/AP line | CR Accounts Receivable | DR Accounts Payable |
| NetAmount formula | Amount − Commission − VAT | Amount + Commission + VAT |
| Commission GL direction | DR Commission, CR Bank | DR Commission, DR Bank total |
| Advanced payment | Customer Advance (liability) | Vendor Advance (asset) |
| Auto-reconciles | Sales Invoices (on post) | No — reconciled separately |
| Unpost requires reason | No | Yes — UnpostedReason stored permanently |
| Balance check on add | No | Yes — bank/treasury balance must be sufficient |
| Journal type | PaymentIn | PaymentOut |
Document Version: 1.0
Namespace: AppsPortal.Application.Finance.PaymentOut
Example Date: 2026-01-28
Currency: Saudi Riyal (SAR)
Total Payment: 1,000.00 SAR