Integration Blog Posts
cancel
Showing results for 
Search instead for 
Did you mean: 
replanta
Newcomer
295

Why idempotency matters

Commerce APIs and corporate ERPs live in noisy networks:

  • Webhooks can be delivered once, twice, or never.

  • Cron and queues can retry at awkward times.

  • Human operators re-process orders when in doubt.

Without idempotency, duplicate pushes create ghost invoices, stock drift, and reconciliation nightmares. Your sync layer must guarantee “exactly-once effects” even if the transport is at-least-once.

WooCommerce ──(OrderCreated Webhook)──▶ Sync API (Ingress)
        ▲                                   │
        │                              Validate + Normalize
        │                                   │
        └──(Health/Retries)◀── Work Queue ◀─┘
                                         │
                                      Workers
                                         │
                                SAP B1 Service Layer / DI-API
                                         │
                                   Result & Audit Log
                                         │
                                  Callback / Notes on Order

Core components

  • Ingress API: receives Woo events or is called by cron. Adds idempotency key to the message (e.g., wc_order_id + stable salt).

  • Work queue: decouples reception from processing; supports backoff and DLQ (dead letter queue).

  • Workers: implement mapping, pre-checks, SAP calls, and idempotent write.

  • Audit log + metrics: every attempt is recorded with correlation IDs.

  • Reconciliation jobs: periodic sanity checks to detect drift (e.g., stock/price mismatches).

Data contract and mapping

Define a versioned contract between Woo and SAP. Keep a living spec (v1, v1.1, …). Typical mappings:

Order header

  • DocDate ← Woo order date (UTC normalized).

  • CardCode ← Customer code (derived from email/metadata or a “generic web” account with address lines).

  • Comments ← Human note + machine hints (payment/ref, applied coupon, shipping method).

  • UDFs ← DNI/NIE, marketing source, consent flags.

Lines

  • ItemCode ← SKU (strict).

  • Quantity ← Int/decimal; enforce unit policy.

  • Price ← Decide: pre-VAT vs after-VAT. Keep one source of truth.

  • DiscountPercent ← Merge order-level and line-level discounts carefully.

Taxes

  • Align tax groups (SAP) with Woo tax classes (name + rate). Don’t hardcode 21% if rates can vary per product/region.

  • When Woo shows prices incl. VAT, ensure you send PriceAfterVAT (or compute net + tax group consistently).

Shipping & fees

  • Model shipping as a non-inventory item with its own tax group.

  • Payment fees (if used) should be explicit items for auditability.

Idempotency strategy

  1. Keying
    Use a stable, deterministic idempotency key per semantic operation. For order creation, a good default is:

"wc:" + order_id + ":comments:v2"
  • edger table (minimal schema)
CREATE TABLE sync_ledger (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  idem_key VARCHAR(128) UNIQUE NOT NULL,
  source VARCHAR(32) NOT NULL,    -- 'wc'
  target VARCHAR(32) NOT NULL,    -- 'sapb1'
  op VARCHAR(32) NOT NULL,        -- 'create_invoice', 'patch_comments'
  status VARCHAR(16) NOT NULL,    -- 'applied','skipped','failed'
  target_docnum VARCHAR(64) NULL, -- SAP DocNum/DocEntry
  checksum VARCHAR(64) NULL,      -- payload hash (optional)
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
​

 

  1. Apply-once guard (PHP example)

function already_applied($pdo, $key){
  $stmt = $pdo->prepare("SELECT status, target_docnum FROM sync_ledger WHERE idem_key=?");
  $stmt->execute([$key]);
  return $stmt->fetch(PDO::FETCH_ASSOC);
}

function mark_applied($pdo, $key, $op, $docnum){
  $stmt = $pdo->prepare("
    INSERT INTO sync_ledger (idem_key, source, target, op, status, target_docnum)
    VALUES (?, 'wc','sapb1', ?, 'applied', ?)
    ON DUPLICATE KEY UPDATE status='applied', target_docnum=VALUES(target_docnum)
  ");
  $stmt->execute([$key, $op, $docnum]);
}

function create_invoice_once($pdo, $payload){
  $key = "wc:".$payload['order_id'];
  if ($row = already_applied($pdo, $key)) {
    // Return the previously created doc (idempotent)
    return ['status'=>'ok','docnum'=>$row['target_docnum'],'idempotent'=>true];
  }
  // ... map Woo → SAP body
  $sap = sap_create_invoice($payload); // call Service Layer/DI-API
  mark_applied($pdo, $key, 'create_invoice', $sap['DocNum']);
  return ['status'=>'ok','docnum'=>$sap['DocNum'],'idempotent'=>false];
}

 

  • Retry/backoff
    Use exponential backoff with jitter (e.g., 5s, 15s, 45s, 2m… up to a ceiling).
    Only retry on transient failures (timeouts, 5xx).
    Stop on validation errors and route to DLQ with a clear reason.
    1.  

 

Observability and audit

Minimum viable signals:

  • Correlation ID: include X-Correlation-ID: wc:<order_id> in all logs and SAP calls when possible.

  • Metrics: processed/min, success rate, median & p95 latency, retries, DLQ size.

  • Structured logs: JSON lines with stage, order_id, idempotency_key, op, outcome, docnum, error_code.

Example (JSON log):

{"ts":"2025-10-21T07:25:00Z","corr":"wc:58291","stage":"worker",
 "op":"create_invoice","idemp":"wc:58291","result":"applied","docnum":"153447"}

ecurity and privacy

  • PII minimization: only store what you need for reconciliation. Mask sensitive fields in logs.

  • Transport: TLS everywhere; verify certificates; set sane timeouts.

  • Auth: never embed long-lived SAP creds in frontend systems. Keep secrets in your sync service.

  • Reconciliation: a periodic job that compares a slice of last N orders between Woo and SAP and emits a report.

Handling prices and stock (the tricky parts)

Prices

  • Decide if Woo is incl. VAT and make the mapping deterministic. If SAP holds net prices:

    • Convert Woo gross → net using the exact tax group.

    • Or switch to PriceAfterVAT in SAP where appropriate (be consistent).

  • Ensure coupon discounts don’t double-apply (line-level vs order-level).

Stock

  • If you maintain multi-warehouse levels, your queue should:

    • Read current availability from SAP (01, LI, etc.).

    • Apply business rules (which warehouse feeds the web?).

    • Push the effective stock back to Woo on a schedule (and after significant changes).

Testing matrix (what to simulate)

  • Duplicate webhook deliveries (2–5 times).

  • SAP API 5xx and slow responses (timeouts).

  • Price with and without VAT; multiple tax classes.

  • Shipping with different tax groups.

  • Partial failures: header accepted, line rejected.

  • Network partitions (queue builds up).

  • Schema evolution: add UDF, new field in comments, deprecate a field.

Minimal worker skeleton (Python-ish pseudocode)

def worker_loop(queue, sap_client, db):
    for msg in queue.consume():
        order = msg.payload
        idem = f"wc:{order['id']}"

        prior = db.find_ledger(idem)
        if prior and prior.status == 'applied':
            ack(msg); continue

        try:
            body = map_woo_to_sap(order)      # normalize/mapping
            doc  = sap_client.create_invoice(body)  # call Service Layer
            db.upsert_ledger(idem, op='create_invoice', status='applied', docnum=doc['DocNum'])
            ack(msg)
        except TransientError as e:
            msg.retry(backoff='expo_jitter')
        except ValidationError as e:
            db.upsert_ledger(idem, op='create_invoice', status='failed', docnum=None)
            msg.to_dlq(reason=str(e))

Rollback & human-in-the-loop

  • No blind deletes. Create compensating actions (credit memo) instead of destructive edits.

  • Provide a small ops dashboard: search by order ID, see ledger status, re-send safe operations with a single click.

What about the checkout side?

If you operate in regions that need national ID validation (e.g., Spain’s DNI/NIE/CIF), implement:

  • Client-side hints to reduce friction.

  • Server-side validation for integrity.

  • Blocks & Classic compatibility (selector names differ).

  • Store validated ID as a UDF in SAP so finance can reconcile.

This reduces address/accounting errors before the sync pipeline receives the order.

Conclusion

The best SAP ↔ WooCommerce integrations succeed not because the API calls are clever, but because the system around them—idempotency, retries, observability, and data discipline—is boringly reliable. Start with a strong contract, a small ledger, and a queue; the rest is iteration.

About the author

Luis Javier Gil is a WordPress/WooCommerce developer and founder at Replanta LC. He builds production-grade integrations (SAP Business One ↔ WooCommerce), performance tooling, and checkout compliance features.