Appearance
Payment In Complete Walkthrough — Example-Based Documentation
Overview
This document demonstrates the complete lifecycle of a Payment In transaction using real payload examples, showing exactly which tables and columns are affected during Add (Draft), Post, and Unpost phases.
A Payment In records money received from a customer (or other payer) into a bank account or treasury. It can optionally:
- Reconcile against one or more outstanding Sales Invoices
- Apply as a Customer Advanced Payment (received before any invoice exists)
- Include a payment method commission (e.g. bank transfer fee, cheque commission) with optional VAT
Example Payload — Customer Invoice Payment (Bank Transfer)
json
{
"paymentInDate": "2026-01-28T16:30:00",
"description": "Payment for SI-2026-0001",
"currencyId": 4,
"rate": 1.0,
"paymentHub": "Bank",
"paymentHubDetailId": 12,
"isCustomerAdvancedPayment": false,
"isAmountIncludesVat": false,
"financialYearPeriodId": "FY-2026-Jan",
"paymentInDetails": [
{
"paidBy": "Customer",
"paidByDetailsId": 433,
"paymentMethodId": 5,
"amount": 1150.00,
"currencyId": 4,
"rate": 1.0,
"costCenters": [],
"paymentMethodDetail": {
"chequeNumber": null,
"chequeDueDate": null,
"bankReference": "TRF-20260128-001",
"commissionType": null,
"commissionValue": 0,
"allowVat": false,
"vatRatio": 0
}
}
]
}Pre-Save Calculations
Detail-Level:
LocalAmount = Amount × Rate = 1150 × 1.0 = 1150
CommissionAmount = 0 (no commission)
VatAmount = 0 (no VAT)
NetAmount = Amount - CommissionAmount - VatAmount = 1150 - 0 - 0 = 1150
NetLocalAmount = NetAmount × Rate = 1150 × 1.0 = 1150
Header Totals:
Amount = Sum of detail amounts = 1150
LocalAmount = Sum of detail local amounts = 1150
NetAmount = Sum of detail net amounts = 1150
NetLocalAmount = Sum of detail net local amounts = 1150Phase 1: Add Payment In (Draft)
Handler: AddPaymentInCommandHandler
Action: Creates a draft payment-in record with no GL or bank impact.
Validations Performed
| Check | Rule | Result |
|---|---|---|
| Payment date in open period | Resolved via GetCurrentPeriodQuery(PaymentInDate) | ✅ Jan 2026 open |
| Currency required | CurrencyId must be provided | ✅ SAR (4) 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 |
| Detail paid-by | PaidBy must be Customer, Vendor, or Other | ✅ Customer |
| Paid-by entity | PaidByDetailsId must reference valid customer | ✅ Customer 433 |
| Payment method | Must reference valid PaymentMethod | ✅ Method 5 (Transfer) |
| Amount > 0 | Each detail amount must be positive | ✅ 1150 > 0 |
| Advanced payment rules | If IsCustomerAdvancedPayment = true, only one detail allowed | ✅ Not advanced |
| 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: PaymentInHeader (INSERT)
Operation: INSERT new record
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | e.g., a1b2c3d4-1234-5678-abcd-ef0123456789 |
| Code | "PI-2026-0001" | Sequence Service | Generated by UpdateLastSequenceCommand |
| PaymentInDate | 2026-01-28 16:30:00 | Payload | Transaction date |
| Description | "Payment for SI-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 | 1150.00 | Calculated | Sum of detail amounts |
| LocalAmount | 1150.00 | Amount × Rate | Base currency amount |
| NetAmount | 1150.00 | Amount - Commission - VAT | Net after deductions |
| NetLocalAmount | 1150.00 | NetAmount × Rate | Base currency net |
| IsCustomerAdvancedPayment | false | Payload | Not an advance payment |
| IsAmountIncludesVat | false | Payload | VAT not included in amount |
| TaxId | NULL | Payload | Tax definition (used for advanced payments) |
| AccountId | NULL | Payload | Override GL account (optional) |
| FinancialYearPeriodId | [Integer DB ID] | GetCurrentPeriodQuery(PaymentInDate) | Resolved from date — payload financialYearPeriodId string is for display only |
| JournalId | NULL | Draft state | Set on post |
| JournalCode | NULL | Draft state | Set on post |
| SourceDocumentType | NULL | Not from SO | Set for system-generated payments |
| SourceDocumentId | NULL | Not from SO | Reference to source transaction |
| SourceDocumentCode | NULL | Not from SO | Source transaction code |
| WorkflowStatus | NULL | No workflow triggered yet | RequestStatus enum |
| ExternalIdentifier | NULL | Not external | External system reference |
| ExternalCode | NULL | Not external | External payment code |
| 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: PaymentInDetail (INSERT)
Operation: INSERT one record per detail line
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | Detail line ID |
| PaymentInHeaderId | [Header GUID] | FK | Parent payment-in |
| PaidBy | "Customer" | Payload | PaidBy enum: Customer / Vendor / Other |
| PaidByDetailsId | 433 | Payload | Customer ID (FK to Customer table) |
| CustomerName | "dubai" | Resolved | Customer display name |
| CustomerNameAr | "دبي" | Resolved | Arabic customer name |
| VendorId | NULL | Not vendor | Set when PaidBy = Vendor |
| SalesManId | NULL | Payload | Optional salesman reference |
| PaymentMethodId | 5 | Payload | References PaymentMethod table |
| Amount | 1150.00 | Payload | In foreign currency |
| LocalAmount | 1150.00 | Amount × Rate | Base currency |
| NetAmount | 1150.00 | Amount - Commission - VAT | After deductions |
| NetLocalAmount | 1150.00 | NetAmount × Rate | Base currency net |
| Rate | 1.0 | Payload | Exchange rate |
| CurrencyId | 4 | Payload | Saudi Riyal |
| ReconciledAmount | 0 | Default | No reconciliation yet |
| AmountToReconcile | 1150.00 | = NetLocalAmount | Full amount available to reconcile |
| AccountId | NULL | Optional | Override GL account for this line |
| CreatedDate | [Current Timestamp] | Audit | Auto-set |
| CreatedBy | [Current User ID] | ISecurityService | Authenticated user |
| TenantId | [Tenant GUID] | ISecurityService | Multi-tenancy |
Table 3: PaymentInMethodDetail (INSERT)
Operation: INSERT one record per detail (stores payment method specifics)
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | Method detail ID |
| PaymentInDetailId | [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-001" | 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 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: PaymentInDetailCostCenter (INSERT)
Operation: NONE — no cost centers provided for this payment
When cost centers are required (account's CostCenterConfig = Mandatory):
| Column Name | Value | Source | Notes |
|---|---|---|---|
| Id | [Auto-Generated GUID] | System | Allocation ID |
| PaymentInDetailId | [Detail GUID] | FK | Parent detail |
| CostCenterId | [e.g. 87] | Payload | Cost center reference |
| Percentage | 100.0 | Payload | Allocation percentage |
| Amount | 1150.00 | Detail.NetLocalAmount × Percentage/100 | Allocated amount |
Summary of Add Phase (Draft)
Tables Affected:
- ✅ PaymentInHeader — 1 INSERT
- ✅ PaymentInDetail — 1 INSERT
- ✅ PaymentInMethodDetail — 1 INSERT
- ❌ PaymentInDetailCostCenter — 0 (no cost centers in this example)
- ❌ GL / Bank / Treasury — NO impact (draft only)
Phase 2: Post Payment In
Handler: PostPaymentInCommandHandler
Action: Finalizes the payment, creates a Journal Entry, records a Bank Transaction, and optionally reconciles Sales Invoices.
Prerequisites Check
| Check | Rule |
|---|---|
| Status = Draft | Cannot post already-posted payment |
| GL account for customer | Customer must have Receivable Account configured |
| GL account for payment method | PaymentMethod must have a GL account (e.g. bank clearing 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 | PaymentInDate period must be open |
Table 1: PaymentInHeader (UPDATE)
Operation: UPDATE existing record
| Column Name | Before (Draft) | After (Posted) | Notes |
|---|---|---|---|
| Status | "Draft" | "Posted" | TransactionStatus enum |
| JournalId | NULL | [Journal GUID] | Sales journal entry ID |
| JournalCode | NULL | "JE-2026-0125" | Journal entry code |
| UpdatedDate | NULL | [Current Timestamp] | Last update timestamp |
| UpdatedBy | NULL | [Current User ID] | User who posted |
Table 2: JournalEntry (INSERT — PaymentIn Journal)
Operation: INSERT via IAccountingPublicApi.AddJournal()
Type: PaymentIn
| Column Name | Value | Notes |
|---|---|---|
| Id | [Auto-Generated GUID] | Journal ID |
| Code | "JE-2026-0125" | Sequence code |
| JournalDate | 2026-01-28 16:30:00 | From PaymentInDate |
| Type | "PaymentIn" | JournalEntryType enum |
| SourceDocument | "PaymentIn" | SourceDocument enum |
| SourceDocumentId | [PaymentInHeader GUID] | Link to payment |
| SourceDocumentCode | "PI-2026-0001" | Payment code |
| Status | "Posted" | Immediately posted |
| TotalDebit | 1150.00 | Sum of debit lines |
| TotalCredit | 1150.00 | Sum of credit lines (balanced) |
| FinancialYearPeriodId | [Period ID] | From payment period |
| Description | "Payment for SI-2026-0001" | From payment description |
Table 3: JournalEntryLine (INSERT × 2 — PaymentIn Journal)
Line 1: Bank Account (Debit)
Money received goes into the bank:
| Column | Value | Notes |
|---|---|---|
| AccountId | [BankAccount.GLAccountId] | GL account of bank account 12 |
| AccountCode | "1020" | e.g. Current Bank Account |
| DebitAmount | 1150.00 | Full received amount |
| CreditAmount | 0 | No credit |
| DebitAmountLocal | 1150.00 | × Rate (1.0) |
| CurrencyId | 4 | SAR |
| CurrencyRate | 1.0 | Base currency |
| CostCenterId | NULL | No cost center for bank |
Line 2: Accounts Receivable (Credit)
Customer's outstanding balance reduced:
| Column | Value | Notes |
|---|---|---|
| AccountId | [Customer.ReceivableAccountId] | AR account for customer 433 |
| AccountCode | "1010" | Accounts Receivable |
| DebitAmount | 0 | No debit |
| CreditAmount | 1150.00 | Clears customer balance |
| CreditAmountLocal | 1150.00 | × Rate (1.0) |
| CurrencyId | 4 | SAR |
| CurrencyRate | 1.0 | Base currency |
| CostCenterId | NULL | No cost center for AR |
Journal JE-2026-0125 — Payment In
DR 1020 Bank Account (Current) 1,150.00
CR 1010 Accounts Receivable 1,150.00
--------- ---------
Total 1,150.00 1,150.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 | Receiving bank account |
| TransactionDate | 2026-01-28 | From PaymentInDate |
| TransactionType | "In" | Money coming in |
| Amount | 1150.00 | Payment amount |
| LocalAmount | 1150.00 | Base currency |
| SourceDocumentType | "PaymentIn" | Reference type |
| SourceDocumentId | [PaymentInHeader GUID] | Link to payment |
| SourceDocumentCode | "PI-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 |
| PaymentInDetailId | [Detail GUID] | Link to payment detail |
| Amount | 1150.00 | Detail amount |
| LocalAmount | 1150.00 | Base currency |
| PaidBy | "Customer" | Payer type |
| PaidByDetailsId | 433 | Customer ID |
Table 6: CustomerFinancial (UPDATE)
Operation: UPDATE — customer's financial summary
| Column | Before | After | Notes |
|---|---|---|---|
| TotalPayments | 0 | 1150.00 | += NetLocalAmount |
| OutstandingBalance | 1150.00 | 0.00 | -= NetLocalAmount |
| AvailableCredit | -1150.00 | 0.00 | CreditLimit - OutstandingBalance |
Summary of Post Phase
Tables Affected:
- ✅ PaymentInHeader — 1 UPDATE (status, journal IDs)
- ✅ JournalEntry — 1 INSERT (PaymentIn journal, immediately Posted)
- ✅ JournalEntryLine — 2 INSERTs (Bank DR + AR CR)
- ✅ BankTransactionHeader — 1 INSERT
- ✅ BankTransactionDetail — 1 INSERT
- ✅ CustomerFinancial — 1 UPDATE
GL Impact:
Account 1020 (Bank Account) DR 1,150.00
Account 1010 (Accounts Receivable) CR 1,150.00Phase 3: Auto-Reconcile Sales Invoice on Post
When a payment is posted and reconciliation is enabled, the system links the payment to the outstanding invoice balance.
Table: SalesInvoiceReconciliation (INSERT)
| Column Name | Value | Notes |
|---|---|---|
| Id | [Auto-Generated GUID] | Reconciliation record |
| PaymentInDetailId | [Detail GUID] | Payment line |
| SalesInvoiceHeaderId | [Invoice GUID] | Reconciled invoice |
| SalesInvoiceBalanceId | [Balance GUID] | Installment reconciled |
| Amount | 1150.00 | Reconciled amount |
| LocalAmount | 1150.00 | Base currency |
| ReconciliationDate | 2026-01-28 | Date of reconciliation |
SalesInvoiceBalance (UPDATE)
| Column | Before | After |
|---|---|---|
| DueBalance | 1150.00 | 0.00 |
| LocalDueBalance | 1150.00 | 0.00 |
SalesInvoiceHeader (UPDATE)
| Column | Before | After |
|---|---|---|
| ReconciledAmount | 0.00 | 1150.00 |
| AmountToReconcile | 1150.00 | 0.00 |
| IsFullyReconciled | false | true |
PaymentInDetail (UPDATE)
| Column | Before | After |
|---|---|---|
| ReconciledAmount | 0.00 | 1150.00 |
| AmountToReconcile | 1150.00 | 0.00 |
Phase 4: Unpost Payment In
Handler: UnpostPaymentInCommandHandler
Action: Fully reverses a posted payment — removes the journal, bank transaction, and reconciliations. Returns to Draft.
Prerequisites Check
| Check | Rule |
|---|---|
| Status = Posted | Cannot unpost a Draft payment |
| Open financial period | PaymentInDate period must still be open |
| Journal reversible | The linked journal must not already be reversed |
| Reconciliations reversible | Any reconciled invoices must be in open periods |
Tables Affected
| Table | Operation | Notes |
|---|---|---|
| PaymentInHeader | UPDATE | Status → Draft, JournalId/JournalCode → NULL |
| JournalEntry | Soft DELETE / Reversed | Journal marked as reversed |
| BankTransactionHeader | Soft DELETE | Bank transaction removed |
| BankTransactionDetail | Soft DELETE | Bank detail removed |
| SalesInvoiceReconciliation | Soft DELETE | Reconciliation records removed |
| SalesInvoiceBalance | UPDATE | DueBalance restored to original |
| SalesInvoiceHeader | UPDATE | ReconciledAmount, AmountToReconcile, IsFullyReconciled reset |
| PaymentInDetail | UPDATE | ReconciledAmount → 0, AmountToReconcile restored |
| CustomerFinancial | UPDATE | TotalPayments and OutstandingBalance reversed |
Payment In with Commission (Cheque Payment)
When a payment method has a commission configured (e.g. 2% bank cheque commission), the journal has additional lines.
Example
json
{
"paymentInDetails": [
{
"paidBy": "Customer",
"paidByDetailsId": 433,
"paymentMethodId": 3,
"amount": 1150.00,
"paymentMethodDetail": {
"chequeNumber": "CHQ-001",
"chequeDueDate": "2026-02-15",
"commissionType": "Percent",
"commissionValue": 2.0,
"commissionAccountId": 6010,
"allowVat": true,
"vatRatio": 15.0,
"vatAccountId": 2030
}
}
]
}Commission Calculations
CommissionAmount = Amount × CommissionValue / 100 = 1150 × 2% = 23.00
VatAmount = CommissionAmount × VatRatio / 100 = 23.00 × 15% = 3.45
NetAmount = Amount - CommissionAmount - VatAmount = 1150 - 23 - 3.45 = 1123.55Updated PaymentInDetail
| Column | Value |
|---|---|
| Amount | 1150.00 |
| NetAmount | 1123.55 |
| ReconciledAmount (available) | 1123.55 |
Journal Entry Lines (4 lines instead of 2)
JE-2026-0126 — Payment In (with Commission)
DR 1020 Bank Account 1,123.55 ← net received
DR 6010 Commission Expense 23.00 ← bank commission
DR 2030 VAT Input (Commission) 3.45 ← VAT on commission
CR 1010 Accounts Receivable 1,150.00 ← clears full invoice
--------- ---------
Total 1,150.00 1,150.00Customer Advanced Payment
An advanced payment is received before any invoice exists. It is held as a liability (deferred revenue / customer deposit).
Key Rules
IsCustomerAdvancedPayment = true- Only one detail line is allowed
- A Tax ID must be provided (for VAT on advances, if applicable)
PaidBymust beCustomer- Cannot be reconciled at creation time — reconciled later when invoice is issued
Journal Entry Lines
JE-2026-0127 — Customer Advanced Payment
DR 1020 Bank Account 1,150.00
CR 2040 Customer Advances 1,150.00 ← liability accountThe AccountId used for the credit line comes from the payment header's AccountId field (the configured advance/deposit GL account), not from the customer's receivable account.
Reconciliation at Invoice Time
When a sales invoice is later issued and linked to this advance:
SalesInvoiceReconciliationrecord created- Customer advance balance reduced
AmountToReconcileon thePaymentInDetaildecremented
Treasury Payment (Cash)
When PaymentHub = Treasury, the flow is identical to Bank but creates a TreasuryTransaction instead of a BankTransaction.
Differences
| Property | Bank | Treasury |
|---|---|---|
PaymentHub | Bank | Treasury |
PaymentHubDetailId | BankAccountId | TreasuryId |
| Transaction created | BankTransactionHeader/Detail | TreasuryTransactionHeader/Detail |
| GL debit account | BankAccount.GLAccountId | Treasury.GLAccountId |
Journal Entry Lines (Treasury Cash)
JE-2026-0128 — Cash Payment In
DR 1001 Petty Cash / Treasury 1,150.00
CR 1010 Accounts Receivable 1,150.00Payment Method Types Reference
| Type | CommissionData | ChequeFields | Notes |
|---|---|---|---|
| Cash | Optional | No | Physical cash received |
| Check | Optional | Required (ChequeNumber + ChequeDueDate) | Post-dated cheques supported |
| 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 |
Multi-Currency Payment In
When a customer pays 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,
"paymentInDetails": [
{
"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-0129 — Payment In (USD)
DR 1020 Bank Account 3,750.00 SAR (= 1000 USD × 3.75)
CR 1010 Accounts Receivable 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 In request
↓
2. AddPaymentInCommandHandler.Handle()
↓
3. Validate: payment date, hub, currency, customer, payment method,
commission fields, cheque fields, cost center sums
↓
4. GetCurrentPeriodQuery(PaymentInDate) → FinancialYearPeriodId
↓
5. Generate sequence code → "PI-2026-0001"
↓
6. Calculate detail amounts:
- LocalAmount = 1150 × 1.0 = 1150
- CommissionAmount = 0
- VatAmount = 0
- NetAmount = 1150
- NetLocalAmount = 1150
↓
7. INSERT PaymentInHeader (Status = Draft)
INSERT PaymentInDetail × 1
INSERT PaymentInMethodDetail × 1
↓
8. SaveChanges → committed
↓
9. Return PaymentInHeaderId
--- DRAFT PHASE COMPLETE ---
10. User clicks "Post"
↓
11. PostPaymentInCommandHandler.Handle()
↓
12. Validate: Draft status, open period, GL accounts configured
↓
13. Build Journal command:
DR Bank GL Account 1150
CR Customer AR Account 1150
↓
14. Call IAccountingPublicApi.AddJournal() → JournalCreatedDto
↓
15. Create BankTransactionHeader + BankTransactionDetail
↓
16. UPDATE PaymentInHeader:
Status = Posted
JournalId = [GUID]
JournalCode = "JE-2026-0125"
↓
17. (If auto-reconcile enabled)
INSERT SalesInvoiceReconciliation
UPDATE SalesInvoiceBalance (DueBalance = 0)
UPDATE SalesInvoiceHeader (IsFullyReconciled = true)
UPDATE PaymentInDetail (ReconciledAmount = 1150)
↓
18. UPDATE CustomerFinancial:
TotalPayments += 1150
OutstandingBalance -= 1150
↓
19. SaveChanges → committed
↓
20. Publish webhook event (reconciliation, if enabled)
↓
21. Return PaymentInHeaderId
--- POST PHASE COMPLETE ---
22. (Optional) User clicks "Unpost"
↓
23. UnpostPaymentInCommandHandler.Handle()
↓
24. Validate: Posted, open period, journal reversible, reconciliation periods open
↓
25. Soft DELETE SalesInvoiceReconciliation records
Restore SalesInvoiceBalance.DueBalance
Reset SalesInvoiceHeader.IsFullyReconciled
Reset PaymentInDetail.ReconciledAmount
↓
26. Soft DELETE BankTransactionHeader / BankTransactionDetail
↓
27. Reverse or soft delete JournalEntry
↓
28. UPDATE PaymentInHeader: Status = Draft, JournalId/JournalCode = NULL
↓
29. Reverse CustomerFinancial changes
↓
30. SaveChanges → committed
--- UNPOST PHASE COMPLETE ---Database Transactions
Add Phase
sql
BEGIN TRANSACTION
INSERT INTO PaymentInHeader VALUES (...) -- Status = Draft
INSERT INTO PaymentInDetail VALUES (...) -- 1 detail
INSERT INTO PaymentInMethodDetail 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 = PaymentIn, Posted
INSERT INTO JournalEntryLine VALUES (...) -- 2 lines (Bank DR + AR CR)
-- Step 2: Create bank transaction
INSERT INTO BankTransactionHeader VALUES (...)
INSERT INTO BankTransactionDetail VALUES (...)
-- Step 3: Reconcile invoice (if enabled)
INSERT INTO SalesInvoiceReconciliation VALUES (...)
UPDATE SalesInvoiceBalance SET DueBalance = 0 ...
UPDATE SalesInvoiceHeader SET IsFullyReconciled = 1 ...
UPDATE PaymentInDetail SET ReconciledAmount = 1150 ...
-- Step 4: Update payment header
UPDATE PaymentInHeader SET Status = 'Posted', JournalId = @JournalId ...
-- Step 5: Update customer
UPDATE CustomerFinancial SET TotalPayments += 1150, OutstandingBalance -= 1150 ...
COMMIT TRANSACTIONDuration: ~150–400ms
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 |
| Advanced payment — multiple details | "Customer advanced payment can only have one detail line" | Reduce to single detail |
| Advanced payment — missing tax | "Tax definition required for customer advanced payment" | Set TaxId on header |
| Cost center sum ≠ 100% | "Cost center allocations must total 100%" | Adjust percentages |
| Amount ≤ 0 | "Payment amount must be greater than zero" | Correct amount |
Post Validation Failures
| Error Condition | Error Message | Resolution |
|---|---|---|
| Not Draft | "Payment must be in Draft status to post" | Already posted |
| Missing customer GL account | "Customer receivable account not configured" | Configure customer 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 |
| 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 |
| Closed period | "Payment period is closed — cannot unpost" | Reopen period |
| Journal already reversed | "Journal entry has already been reversed" | Cannot reverse twice |
| Reconciliation in closed period | "Reconciled invoice is in a closed period" | Reopen the invoice's period |
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 number generation, current branch/company, financial period |
IAccountingPublicApi | Creates Journal Entries for posted payments |
IPaymentInProcessingService | Encapsulates payment creation, posting, journal building, commission logic |
Payment In vs Payment Out
| Property | Payment In | Payment Out |
|---|---|---|
| Direction | Money received | Money sent |
| Typical payer | Customer | Vendor |
| Bank line DR/CR | DR Bank | CR Bank |
| AR/AP line DR/CR | CR Accounts Receivable | DR Accounts Payable |
| Reconciles with | Sales Invoices | Purchase Invoices |
| Advanced payment | Customer Advance (liability) | Vendor Advance (asset) |
Document Version: 1.0
Namespace: AppsPortal.Application.Finance.PaymentIn
Example Date: 2026-01-28
Currency: Saudi Riyal (SAR)
Total Payment: 1,150.00 SAR