Invoice & Credit Note Flow
This page traces the complete lifecycle of a sale — and of a refund — through the middleware, step by step.
Flow 1: Invoice upload (the core flow)
POS Terminal
│ POST /pckzra/api/taxint/pos/upload
│ Headers: Authorization: Bearer <jwt>, Content-Type: application/json
│ Body: StandardInvoicePayloadDto (Taxint format)
▼
RequestResponseLoggingFilter
│ Generates transaction_id (UUID), logs raw request to api_transactions
▼
JwtAuthenticationFilter
│ Extracts Bearer token; validates against Oracle IDM JWKS (when enabled)
▼
TaxintController.uploadInvoice()
├── 1. Extract SellerName (root SellerInfo → FolioInfo.SellerInfo)
│ Missing → HTTP 400 "SellerName is required"
├── 2. Validate FiscalFolioId present
│ Missing → HTTP 400 "FiscalFolioId is required"
├── 3. Look up store registration by SellerName
│ Not found → HTTP 403 "Store not registered"
│ Inactive → HTTP 403 "Store is inactive"
├── 4. Tag the transaction with the store ID
├── 5. Convert Taxint → Chrilan (ConversionService)
├── 6. Submit to Chrilan: POST /invoice/create (per-store API key)
├── 7. Convert Chrilan response → Taxint, save FiscalInvoice record
├── 8. Log the response (status, body, timing)
└── 9. Return UploadResponseDto to the POS (HTTP 200)
What the POS gets back
| Response element | Source | Purpose |
|---|---|---|
fiscalFolioNo | Chrilan rcptNo | The ZRA receipt number printed on the receipt |
QR_CODE_URL / QR_CODE (base64) | Chrilan qrCodeUrl, image fetched and encoded | Customer-verifiable QR code |
sdcId | Chrilan sdcId | Smart Device Controller ID |
mrcNo | Chrilan mrcNo | MRC number |
rcptSign | Chrilan rcptSign | Digital receipt signature |
taxData / COLL_TAX1 | Chrilan taxData | Tax breakdown for the receipt |
intrlData, vsdcRcptPbctDate | Chrilan | Internal data and publication date |
BuyerHouseNo/Street/Province | Pass-through from the request | Buyer address for POS check printing |
Flow 2: Credit note / refund
When a sale is reversed, ZRA requires the refund to reference the original invoice. The middleware handles this automatically:
POS sends invoice with FolioType = credit note indicator
▼
ConversionService
├── Detects refund from FolioHeaderInfo.FolioType
├── Looks up the original invoice:
│ FiscalInvoiceRepository.findByFiscalFolioId(originalFolioId)
├── Extracts from the stored record:
│ original receipt_number → refundNumber
│ original sdc_id → refundReceiptSDC
└── Sets on the Chrilan request:
refund = true
refundNumber, refundReceiptSDC, refundResCode = "06"
Why fiscal_invoices exists
Without the original
rcptNo and sdcId, ZRA cannot link a refund to its sale. This is why every issued invoice is persisted locally: audit number, receipt number, SDC ID, store, TPIN, refund flag and buyer address fields.
Negative quantities and amounts on credit notes are converted to absolute values before submission.
Error handling matrix
| Condition | HTTP | Notes |
|---|---|---|
| Missing SellerName | 400 | Checked in all payload locations |
| Missing FiscalFolioId | 400 | Required for traceability |
| Store not registered / inactive | 403 | No fallback key — see Managing stores |
| Payload validation failure | 400 | Field-level errors returned as a list |
| Conversion failure (missing required data) | 400 | Logged to conversion_logs with mapping rules |
| Chrilan API error | 500 | Wrapped in ChrilanApiException with code/message/type and full body |
| Chrilan timeout | 500 | 30-second timeout |
Idempotency and retries
- Every invoice carries a generated
localid(UUID) so Chrilan can correlate submissions. - The
auditNumber(from the POS cashier number) identifies the transaction across systems. - Chrilan calls are configured with retry (3 attempts, 1s backoff) for transient network failures.
Timing budget
A typical successful upload completes in a few seconds: local validation and conversion are sub-second; the dominant cost is the Chrilan round trip (30s timeout) plus the QR code image fetch. Response times per transaction are visible in Reports.