Skip to main content

POS Adapter Architecture

SalesArck normalizes Square and Clover payments in apps/api/src/services and ingests both through the same core.

Canonical Transaction Contract​

Provider payloads are normalized to NormalizedTransaction (packages/types/src/pos.ts):

{
providerTransactionId: string,
provider: "square" | "clover",
amountMinorUnits: number,
currency: string,
transactionAt: string,
customerRef?: {
mobile?: string,
email?: string,
externalCustomerId?: string
},
lineItems?: [...]
}

Current Normalization Components​

square-adapter.ts​

Responsibilities:

  • read webhook/list-payment record variants (snake_case and camelCase)
  • ignore non-completed payment records
  • map money fields and timestamps
  • extract customer references when available

square-api.ts​

Responsibilities:

  • ListPayments pagination API
  • optional RetrieveCustomer enrichment API

Clover fetch and poll​

Clover webhooks are notification-only. The processor fetches each payment from Clover's REST API. POST /api/v1/internal/cron/clover-poll-payments lists payments modified since clover_payments_watermark_at and uses the same ingestion core.

ingestion.ts​

Responsibilities:

  • idempotent ingest orchestration
  • customer identity resolution
  • transaction storage and reward application

Processing Outcomes​

Ingestion reports one of:

  • credited
  • stored_anonymous
  • duplicate
  • no_rule_no_credit

These outcomes make provider noise and business impact explicit.

Idempotency Strategy​

Two layers are used:

  1. application-level idempotency key (idempotency_keys table)
  2. schema-level transaction unique constraint (transactions_provider_dedup)

This protects against retries and race conditions.

Clover Integration Status​

Clover OAuth, webhook receive, REST payment fetch, customer enrichment, ingestion, and payment polling are implemented. Both providers share the same executeIngestion() core. Clover-specifics (notification-only webhooks, no refresh token, signature fail-closed in production) are documented in POS Integrations.

Why This Pattern Works​

  • Provider-specific payload complexity is isolated.
  • Reward and wallet services remain provider-agnostic.
  • New providers can be added by implementing normalize + ingest integration without rewriting reward logic.
Written byDhruv Doshi