AploDelivery Partner API — портал разработчика · v2 (2026-10-16)

AploDelivery Partner API — full website scope (v1 extension) — implementation spec

Планируется

Status: scope approved by the owner on 2026-09-23. It covers every process the website needs: menu and stop-list with iiko as source of truth, points and delivery zones, AploCard loyalty, payment, and guest profile. The ViL website developer integrates it. The docs must be corporate-grade (§9). Builds on PARTNER_API_SPEC.md (already live): conventions, auth, errors, scoping, webhooks and outbox are unchanged unless stated. All new routes stay under /v1 because nobody has integrated yet. The version policy (§9) starts from this release.

0. Architecture

website backend ──HMAC──▶ AploDelivery ──┬─▶ iikoCloud  (menu, prices, stop-lists, payment types, zones*, is_alive)  — read, cached
                                          ├─▶ AploCard   (HMAC POS API: calculate, hold/confirm/cancel, coupons, certificates, accrue)
                                          └─▶ POS plugin (order create, statuses, cancel, streets)     — existing claim protocol

New Go packages (layering Handler → Service → Repository, as today): internal/iikocloud (client, v2 auth with v1 fallback, retry/backoff, 429 handling) · internal/catalog (sync worker + repo) · internal/loyalty (AploCard HMAC client only - typed errors, Fake for tests) · internal/checkout (pure pricing engine + signed calculationId token; DeliveryPricer interface, ZeroPricer stub for the parallel zones branch) · internal/loyaltysaga (background worker: outbox confirm/cancel/accrue, two reaper sweeps) · internal/ratelimit (in-memory per-key limiter). Migrations 006_catalog, 007_points (both already live). Phase 3 shipped as migration 009_loyalty (register version 9), not the originally planned 007_checkout_loyalty/008_guest_ratelimit: 008 is reserved for the parallel zones branch (phase 2), and guest rate limiting turned out to need no table (in-memory is sufficient for the current single-instance deployment - see §4 deviations). 009 adds delivery_loyalty_saga, delivery_loyalty_outbox, delivery_loyalty_audit, delivery_loyalty_org_mapping, and widens delivery_webhook_outbox.event_type's CHECK constraint for the 4 new webhook events.

1. Catalog

Работает

1.1 GET /v1/points/{pointId}/menu

Full sellable menu for the point, merged with the stop-list.

{"pointId":"3","revision":"2026-09-23T10:00:00+03:00#a1b2","currency":"RUB",
 "categories":[{"id":"…","name":"Роллы","sort":10,"parentId":null}],
 "items":[{"id":"<iiko productId>","externalId":"<site dish id if mapped>","categoryId":"…","name":"…","description":"…",
   "price":{"amountMinor":42000,"currency":"RUB"},"weightGrams":230,"nutrition":{"kcal":…,"protein":…,"fat":…,"carbs":…,"per":"100g"},
   "imageUrl":"https://…","available":true,"unavailableReason":null,"sizes":[],"modifierGroups":[],"tags":["new","spicy"]}]}

1.2 GET /v1/points/{pointId}/stop-list

Light, frequent read: {"pointId","revision","items":[{"id","balance"}]}. Webhook stoplist.updated.

1.3 GET /v1/catalog/revision

{"points":{"3":{"menu":"…","stopList":"…"}}} gives a cheap change detection for all points.

2. Points

Работает

2.1 GET /v1/points (extended, backward compatible)

Adds name, address, location{lat,lon}, phone, timezone:"Europe/Moscow", schedule[{weekday,open,close}], isOpenNow, acceptsOrders (= route active ∧ terminal alive ∧ within schedule), deliveryTypes:["courier","pickup"]. Point metadata that iiko lacks (display name, phone, schedule) lives in our delivery_points table, managed by operators (§8), seeded from the site's GET /api/rests data once.

2.2 GET /v1/points/{pointId}/status

{"acceptsOrders","terminalAlive","isOpenNow","reason"}. Webhook point.status_changed.

3. Delivery zones and address

Работает

3.1 POST /v1/delivery/resolve

Input {"location":{"lat","lon"}} and/or {"address":{"city","street","house"}} plus deliveryType. Output: {"serviceable":true,"pointId":"3","zoneId":"…","minOrder":{…},"deliveryFee":{…},"freeDeliveryFrom":{…},"etaMinutes":{"min":45,"max":60},"street":{"status":"ok|ambiguous|not_found|unknown","name":…}}. Point selection: the zone that contains the point wins. On overlap, lowest priority, then nearest point. A street check (existing §1.4 logic) runs against the chosen point's POS dictionary. serviceable=false + reason when outside all zones or the point is not accepting orders.

3.2 GET /v1/points/{pointId}/delivery-zones

{"zones":[{"id","name","polygon":[[lat,lon],…],"minOrder","deliveryFee","freeDeliveryFrom","etaMinutes","priority","active"}]}.

3.3 Zone source — decided: ours (B)

The 2026-09-23 probe returned deliveryZones: [] and restrictions: [] for all 26 ViL orgs, defaultMinSum=0 and no delivery-fee product. The only hint was a Google My Maps link (deliveryRegionsMapUrl). So zones live in our delivery_zones table. They are imported once from the site admin export (the site developer provides JSON/CSV) or from the Google My Maps KML, then managed in our operator admin (§8). The site stops using its own zones. iiko defaults are used as fallback ETA: courier 45 min, pickup 90 min. The contract (§3.1–3.2) is identical in both cases.

4. Checkout pricing

Работает

4.1 POST /v1/checkout/calculate

No side effects. Input:

{"pointId":"3","deliveryType":"courier","location":{…},"items":[{"id":"<iiko productId>","quantity":"2"}],
 "guest":{"phone":"+7…"},"coupon":"ABC123","bonusToSpend":{"amountMinor":10000,"currency":"RUB"},"deliverAt":"…"}

Output: line prices from our catalog (never from the request), subtotal, programDiscount (automatic AploCard loyalty-program discounts), coupon{status, discount}, bonus{requested, maxSpend, spend}, deliveryFee, minOrder{met, missing}, total, warnings[] (e.g. item in stop-list, point closed), and calculationId (a signed token over the inputs and result, valid 15 min). Shipped coupon.status values: not_requested | applied | invalid - the engine's /calculate preview does not distinguish "invalid" from "expired" (no separate signal in its response), so both collapse to invalid here; a distinct expired status would need a dedicated engine field this phase does not have. Delivery is priced as a line after loyalty discounts. The existing gotcha is that promotions discount the delivery line. Decide per AploCard rule: the delivery line is excluded from discount bases (documented). Delivery-fee-line follow-up (owner decision 2026-09-23), shipped: pricing this fee into total was never the gap - deliveryFee/total above already reflected it. The gap was that the DeliveryCommand sent to the POS at order creation carried no line for it at all, so the created iiko order's own total fell short by the fee and the plugin-side POS invariant re-check could never balance. Closed with an additive deliveryFeeLine{iikoProductId, priceMinor} on the wire (domain.CommandDeliveryFeeLine, plugin/WIRE_CONTRACT.md's new section) - iikoProductId from DELIVERY_FEE_PRODUCT_ID (tenant-wide env, ViL's live «Доставка Приложение» Service product acdca4b3-8329-42ce-b8d2-5ab335a63b3d by default), priceMinor the zone fee actually priced. Absent for pickup and for a zero fee. internal/service.verifyMappedLoyaltyInvariant now takes the persisted payload.DeliveryFeeMinor as an explicit parameter rather than reading checkout.Result.DeliveryFeeMinor implicitly, so the check verifies what will actually reach the plugin, not merely what was priced a moment earlier.

4.2 Order creation with checkout (POST /v1/orders, extended)

New optional fields: calculationId, coupon, bonusToSpend, payment, deliveryType, location, comment, persons, callBack:false.

4.3 Implementation notes and deviations (phase 3, shipped 2026-09-23)

Verified against the live engine source (not assumed) and worth recording here so the next person does not re-derive them:

5. Guest

6. Webhooks (extended)

New event types: menu.updated, stoplist.updated, point.status_changed, order.payment_recorded, order.loyalty_confirmed, order.loyalty_released, order.accrued. Same signing, outbox, retries and dedupe as today. Subscriptions are per event type in the credential config (webhookEvents, default all).

7. Operability

8. Operator/admin (minimal, AploKey SSO, roles)

Point metadata (name, phone, schedule), zones (source B), payment-type mapping per point, partner credentials (issue/rotate API key and webhook secret, event subscriptions, point allowlist). The admin UI is a later phase. The first cut is an admin JSON API + CLI. authz → Sonnet gate.

9. Corporate-grade documentation (deliverable, same priority as code)

Published developer portal served by the service at /docs (static, versioned):

  1. API reference: OpenAPI 3.1 (single source of truth, lint-clean with Spectral), rendered with Redoc. Every field has a description, example, units and enum meanings.
  2. Guides (RU): Quick start (first signed call in 10 min); Authentication and signing (with test vectors); Checkout flow end-to-end (sequence diagram); Menu and stop-list sync strategy; Delivery zones and address resolution; Loyalty (coupons, bonuses, trust model); Payments (online before creation, cash, change); Order lifecycle and statuses (state diagram, customer-facing RU labels); Cancellation rules; Webhooks (verification, retries, dedupe); Errors catalogue (every code, cause, action); Rate limits; Idempotency; Go-live checklist.
  3. Policies: versioning and deprecation (additive changes any time; breaking changes only in a new major, with ≥ 90 days notice), changelog (dated, per release), support and escalation contacts, SLA/SLO (availability target, sync freshness), security (key storage, rotation, IP allowlist option, PII).
  4. Sandbox: separate tenant and keys on staging, with a POS simulator that walks orders through statuses (configurable delays, failure scenarios: stop-list, price change, cancel rejected), a fake point with a menu, and zones.
  5. SDK and examples: Go SDK (pkg/ingressclient, all endpoints + webhook verification), a Postman collection, curl examples for every endpoint, and a signature test-vector file.
  6. Docs are tested: examples in the guide are executed in CI against the sandbox (doc-tests), and the OpenAPI is validated against real handler responses (contract tests).

10. Phases and acceptance

PhaseContentDone when
0Live iiko probe (read-only) — done 2026-09-23 (/home/dev/aplodelivery-rollout/iiko-probe-2026-09-23.md)zones = ours; menu = nomenclature; ECARD only; v1 token works
1iikocloud client + catalog sync + menu/stop-list/points/status/revisionmenu for Боброво matches iiko; stop-list freshness ≤ 60 s measured; contract tests green
2Zones + resolve (A or B)resolve returns the right point for 20 real addresses of 5 points
3Checkout calculate + order extension + loyalty saga + payment on POS (plugin) + reaperServer side done 2026-09-23 (branch feat/aplodelivery-v2-checkout): unit/integration tests cover calculate pricing, tampered-item-price rejection, expired/tampered-token rejection, online-without-paid rejection, full coupon+bonus saga happy path, hold-failure compensation, crash-resume (retry reuses held amounts, zero duplicate AploCard calls), per-leg-idempotent confirm/cancel retry, both reaper sweeps, and accrue's transport-error read-back. Still open: an actual ECARD prepayment on a real POS order (needs the plugin build with the wire-contract additions in a companion change, and a live/sandbox AploCard tenant - neither exists in this branch's scope) and a genuine end-to-end sandbox run once the plugin side lands.
4Guest endpoints + audit + rate limits + webhooks v2authz tests (cross-partner, cross-source, phone scoping)
5Sandbox simulator + developer portal + SDK + Postman + doc-testsa developer can go from zero to a sandbox order using only the portal

Security gate: credentials, scoping, guest/loyalty endpoints, audit and rate limiting are written and reviewed by Sonnet (web-developer). Everything else follows the peer-review chain.