# Invoices: Adapters

**Date:** 2026-05-30  
**Status:** Draft  
**Depends on:** `invoices-core`, `foundation-auth-rbac`, `settings-module`  
**Referenced by:** `settings-module`, `billing-module`

---

## Overview

Five Israeli accounting/invoicing platforms supported as optional adapters. When enabled, tax invoices issued in Zync are pushed to the configured provider. Proforma flow stays in Zync; only `TAX_ISSUED` and `PAID` events sync outward.

---

## Adapter Interface

```ts
interface InvoiceAdapter {
  id: 'morning' | 'icount' | 'rivhit' | 'invoice4u' | 'easycount'
  name: string

  // Validate credentials (called on save in settings)
  testConnection(credentials: AdapterCredentials): Promise<{ ok: boolean; error?: string }>

  // Push a tax invoice to the provider; returns external ID
  createInvoice(invoice: InvoicePayload, credentials: AdapterCredentials): Promise<{ externalId: string; externalUrl?: string }>

  // Push a credit note (חשבונית זיכוי)
  createCreditNote(invoice: InvoicePayload, credentials: AdapterCredentials): Promise<{ externalId: string }>

  // Record payment on an already-pushed invoice
  recordPayment?(externalId: string, payment: PaymentPayload, credentials: AdapterCredentials): Promise<void>

  // Pull invoice status from provider (for reconciliation cron)
  getStatus?(externalId: string, credentials: AdapterCredentials): Promise<{ status: string; paidAt?: Date }>
}
```

### `InvoicePayload`

```ts
interface InvoicePayload {
  internalId: string
  invoiceNumber: string
  issueDate: string         // ISO date
  customer: {
    name: string
    vatId?: string          // ח.פ./ע.מ.
    email: string
    address?: string
  }
  lines: {
    description: string
    quantity: number
    unitPrice: number
    discount: number
    total: number
    taxable: boolean
  }[]
  vatRate: number
  subtotal: number
  vatAmount: number
  total: number
  currency: 'ILS'
  notes?: string
}
```

---

## Supported Adapters

### Morning (Green Invoice)

API: `https://api.greeninvoice.co.il/api/v1`  
Auth: API key + secret from Morning dashboard.  
Push: `POST /documents` — creates document (type `320` = tax invoice, `305` = credit note).  
Returns: `{ id, number, url }`.

```ts
credentials: {
  apiKey: string     // stored encrypted in DB
  secret: string     // stored encrypted in DB
}
```

Morning is the most common IL provider. Adapter also maps to incoming payment webhooks if tenant enables Morning billing (see `billing-module`).

### iCount

API: `https://api.icount.co.il/api/v3.php`  
Auth: `cid` (company ID) + `user` + `pass` (encrypted).  
Push: `POST /api/v3.php?action=doc_create` with form params.  
Returns: doc ID.

### Rivhit

API: `https://secure.rivhit.co.il/API/PurchaseGroupAPI.svc`  
Auth: API key per company.  
Push: `CreateInvoice` SOAP/REST endpoint.

### Invoice4u

API: `https://api.invoice4u.co.il/Services/InvoicesService.svc`  
Auth: token-based.  
Push: `CreateDoc` endpoint.

### Easycount

API: `https://app.easycount.co.il/api`  
Auth: API key.  
Push: `POST /invoices` endpoint.

---

## Adapter Credentials Storage

Credentials stored encrypted in DB (`adapter_credentials` table) using `INTEGRATION_ENCRYPTION_KEY` (AES-256-GCM, same pattern as SMTP credentials).

```sql
adapter_credentials (
  id UUID PRIMARY KEY,
  tenant_id UUID NOT NULL,
  adapter_id TEXT NOT NULL,           -- 'morning' | 'icount' | ...
  credentials BYTEA NOT NULL,         -- AES-256-GCM encrypted JSON
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now(),
  UNIQUE (tenant_id, adapter_id)
)
```

---

## Push Lifecycle

When an invoice transitions to `TAX_ISSUED`:

1. Check if tenant has an active invoice adapter configured
2. If yes: enqueue `invoice.push` job (`QUEUE.send({ type: 'invoice.push', invoiceId })`)
3. Queue consumer:
   a. Load invoice + credentials
   b. Decrypt credentials
   c. Call `adapter.createInvoice(payload, credentials)`
   d. On success: store `external_id` + `external_provider` on invoice record; log to `integration_sync_logs`
   e. On failure: retry up to 3× with exponential backoff; on final failure: create in-app notification for tenant admin

When invoice transitions to `PAID` and `recordPayment` supported: enqueue `invoice.payment_sync` job.

Credit note: same push flow but calls `adapter.createCreditNote`.

---

## Invoice Automation Settings (`/settings/integrations/invoicing`)

> This spec owns the settings data shape (`InvoiceAutomationSettings`) and the adapter interface; spec 127 (`invoice-adapter-setup-ui`) owns the per-adapter setup wizard UI on the same `/settings/integrations/invoicing` page.

Per-tenant settings:

```ts
interface InvoiceAutomationSettings {
  adapter: InvoiceAdapterId | null
  autoSyncOnTaxIssue: boolean          // push to adapter on TAX_ISSUED (default true if adapter set)
  autoDraftFromRetainer: boolean       // auto-create draft when retainer depletes
  autoSendRetainerInvoice: boolean     // send (not just draft) retainer invoice automatically
  taskStatusTrigger: {
    enabled: boolean
    statusId: string                   // task status that triggers invoice
    invoiceType: 'draft' | 'sent'
  } | null
}
```

Stored as JSONB in `tenant_settings` (see `settings-module`).

---

## Reconciliation Cron

`/api/cron/invoice-reconcile` — daily (CRON_SECRET).

For each tenant with adapter configured: calls `adapter.getStatus()` on `TAX_ISSUED` invoices older than 24h. If provider reports paid: update invoice status to `PAID`, fire `invoice.paid` webhook.

Not all adapters support `getStatus` — those without it rely on manual "Record payment" action or inbound webhooks from payment processors (see `billing-module`).

---

## Sync Logs

Every push attempt logged to `integration_sync_logs`:

```sql
integration_sync_logs (
  id UUID PRIMARY KEY,
  tenant_id UUID NOT NULL,
  adapter_id TEXT NOT NULL,
  entity_type TEXT NOT NULL,   -- 'invoice' | 'credit_note' | 'payment'
  entity_id UUID NOT NULL,
  direction TEXT NOT NULL,     -- 'push' | 'pull'
  status TEXT NOT NULL,        -- 'success' | 'error'
  request_payload JSONB,
  response_payload JSONB,
  error_message TEXT,
  created_at TIMESTAMPTZ DEFAULT now()
)
```

Visible in admin tenant detail (Services Log tab) and tenant settings (per-adapter status card).

---

## Permissions

| Action | Required permission |
|--------|-------------------|
| Configure adapter | `settings:write` |
| View sync logs | `settings:read` |
| Manual push retry | `invoices:write` |

---

## API Endpoints

```
GET    /api/settings/integrations/invoicing          → current adapter config
PUT    /api/settings/integrations/invoicing          → save adapter + credentials
POST   /api/settings/integrations/invoicing/test     → test connection
GET    /api/settings/integrations/invoicing/logs     → sync log (paginated)
POST   /api/invoices/:id/push                       → manual push to adapter (staff-triggered retry)
```

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| Push on TAX_ISSUED only | Not on draft/proforma | Providers issue real documents on push; can't unsend |
| Queue-based push | Not synchronous | Adapter calls can be slow/flaky; don't block invoice issuance UI |
| Per-tenant single adapter | One adapter at a time | Multi-adapter sync is complex; tenants switch providers, not multi-home |
| Credentials encrypted in DB | `INTEGRATION_ENCRYPTION_KEY` | Same pattern as SMTP — not in env vars (per-tenant secrets) |
| Credit notes via adapter | Reuse same push flow | Provider must issue matching credit note for legal compliance |
