---
title: "Shopark license reconciliation"
canonical_url: "https://ainode.run/docs/shoply-partner"
type: "doc"
locale: "en"
description: "Authenticated reconciliation, failure recovery and rollout order for Shopark stores."
---

# Shopark license reconciliation
> Authenticated reconciliation, failure recovery and rollout order for Shopark stores.

APay owns accounts and subscriptions. Shopark's Saas application links the merchant and applies the current license to stores through Go RPC. Core payment and subscription APIs remain the billing authority.

## Plan catalog and license snapshots

The partner contract in Shopark `plugins/app/Saas/PARTNER_APAY.md` governs external licenses; native Shopark billing rules do not replace APay billing. `app/extensions/shopark/shared/plans.ts` is the APay mapping source, re-exported by `app/themes/shopark/data/plans.ts`. Theme and management extension share the signed client, entitlement resolver, durable versions, readiness checks and reconciliation storage; admin re-pushes cannot bypass explicit plan keys. Public sales include seven price options, all in CNY:

| planKey | Display name | Month | Year | planCode |
| --- | --- | --- | --- | --- |
| free | Unpaid / expired | Not sold | Not sold | free |
| showcase | Showcase | Unavailable | 399 CNY/year | showcase-yearly |
| starter | Starter | 199 CNY/month | 1,999 CNY/year | starter-monthly / starter-yearly |
| professional | Professional | 299 CNY/month | 2,999 CNY/year | professional-monthly / professional-yearly |
| premium | Premium | 599 CNY/month | 5,999 CNY/year | premium-monthly / premium-yearly |
| enterprise | Enterprise Custom | Contract | Contract | enterprise-custom (explicit enterprise key) |

Subscription product metadata declares `shopark_plan_key`, `shopark_plan_code`, `shopark_store_limit`, `level`, and `subscription_cycle`. The canonical ranks are 0–5 in table order. Existing known codes remain readable: bare `enterprise` and `enterprise-monthly/yearly` mean **premium**, while explicit `shopark_plan_key=enterprise` always means Enterprise Custom. Unknown keys/codes are excluded; do not infer Enterprise Custom from a display name. Free and Enterprise Custom are never listed as public fixed-price products. The public catalog also excludes conflicting prices, cycles, ranks or locale price overrides.

Every outgoing license contains `{ planCode, planKey, expireAt, maxStores, version }`. `GET /api/shopark/entitlement` now includes `data.planKey`; it computes only active, already-started subscription periods. Refund/revocation, effective cancellation and expiry fall back to `free`; cancellation at period end retains paid rights until that date. `expireAt=null` means long-term validity (Shopark stores 2099-12-31T23:59:59Z), not a trial. The native 14-day Professional trial is unavailable through APay partner snapshots.

Versions use durable per-user compare-and-swap settings `shopark_license_version.`, allocated before reading subscriptions. They increase across simultaneous pushes, process restarts and clock rollback. These settings are private and must survive releases and rollbacks. They require no core schema migration.

Entitlements apply to all stores owned by the APay user. `maxStores` is a creation limit, separate from per-store resources. Store creation and capacity displays use Shopark's returned quota, including platform caps and pending creation reservations. An uncertain creation retry retains its original request ID and frozen parameters, including when reopening the dialog. `replayed=true` refers to the same store; `licenseApplied=false` requires pending-sync messaging. `store/list` additionally returns per-store operating metrics (`stats`: total/active products, 30d/total paid orders, 30d/total GMV, buyers, 30d UV/PV, `lastOrderAt`) and a merchant-wide `summary` (including opened region/timezone and currency distribution), while `store/overview` provides the platform-wide store footprint and operating breakdown for `/admin/plugins/shopark/stores`.

| planKey | Products / active products | Content pages | Domains | Staff (excluding owner) | Storage | AI points/month |
| --- | --- | --- | --- | --- | --- | --- |
| showcase | 20 | 5 | 1 | 0 | 200 MB | 0 |
| starter | 500 | Uncapped | 1 | 2 | 5 GB | 100 |
| professional | 5,000 | Uncapped | 3 | 5 | 20 GB | 300 |
| premium | 20,000 | Uncapped | 10 | 15 | 100 GB | 1,000 |
| enterprise | Current Premium baseline; contract overrides are not implemented |  |  |  |  |  |

Storage is decimal (1 GB = 1,000,000,000 bytes). Shopark meters resources and AI; APay does not duplicate metering. AI points are granted monthly even for annual subscriptions, do not roll over, and upgrades only add the current-month difference without clearing used points. **Known gap:** APay partner AI pools refresh by UTC calendar month because the partner contract has no billing-period anchor. Do not promise billing-anniversary grants. Shopark has no partner resource-usage endpoint, so APay cannot show real usage before a downgrade.

## Existing-store binding and downgrades

Binding uses challenge → verify → confirm. Store rows show localized names from `planKey` (known `edition` aliases only as fallback). Verify shows each store's `currentPlanKey/currentExpireAt → nextPlanKey/nextExpireAt`. Any row with `downgrade=true` requires an unchecked acknowledgement; confirm sends `acknowledgeDowngrade=true` only after the user checks it. `bind:acknowledge_required` remains a visible business error. `failed[]` produces “Pending sync”, independently of account linking, and reconciliation repairs the license.

Binding stops renewal of the original Shopark subscription and switches billing to APay. A lower partner snapshot takes effect immediately: existing excess data stays, but new products, uploads, staff, domains and AI use may be blocked. APay preserves its core rule that lower-plan purchases are blocked during an active subscription; the theme explains this before attempting checkout. This release does not add prorated billing or a scheduled downgrade purchase flow.

`GET /api/shopark/cron/license-sync` requires `Authorization: Bearer $CRON_SECRET` in every environment. It returns `{ data: { busy, checked, pushed, failed, pending, cursor, lastFullScan } }`; a concurrent lease holder returns only `{ data: { busy: true } }`. Missing authentication returns 401, missing partner configuration returns 503, and outstanding retries return 503 with the batch report. Storage or transport failures remain errors.

The task scans users by ascending ID, keeps a durable cursor, retries failed users and restarts the sweep when it reaches the end. A batch scans up to 100 users and retries up to 25 users within a 45 second processing budget. Paid users can recover failed initial provisioning; free users only synchronize existing Shopark accounts. Each push recomputes the complete current subscription state, including refunds, cancellations and expiry. There is no three day cutoff. Shopark reports unfinished creation as `pendingRequests`, which also enters the retry queue. State is stored in the private `shopark_license_reconcile_state` setting; never register it as public.

Configure the core scheduler with path `/api/shopark/cron/license-sync`, method `GET`, schedule `5` (minutes), `useCronSecret=true`, and `enabled=true`. Serverless deployments use an external scheduler with the same header. The full sweep duration depends on user count, latency and frequency. Verify a completed sweep and zero pending users; an old successful HTTP response is insufficient.

Persist a minute interval as a JSON number (`5`), not a string (`"5"`); an invalid interval falls back to daily. For loopback development with Shopark tenant paths enabled and local IP access allowed, use `SHOPARK_PARTNER_API_URL=http://127.0.0.1:5490/1/app/saas/partner/apay` when the configured super store is ID 1. This supplies tenant routing without a temporary request header. Restart the local APay process to reload environment variables; production uses its configured super-store domain.

Only `license/sync` retries an explicit HTTP 429, up to three attempts within the existing 25-second request deadline. Each attempt signs a fresh nonce while retaining the same complete snapshot and version. Retry-After seconds are bounded to five seconds; other values use a one-second delay. Network uncertainty, business rejection, store creation and binding are not automatically replayed. Exhausted attempts and unfinished creation remain failures in the durable queue.

Store creation freezes its parameters before calling Shopark. Retry uncertain results with the same `requestId`; after ten minutes an interrupted request can recover the existing store by domain and its random administrator identity. Background reconciliation also resumes the request if the browser loses its ID. Unfinished creation reserves capacity and cannot be bypassed with another request. `licenseApplied=false` means the store exists while authorization is pending. Linking an existing merchant likewise reports `failed[]` independently of successful account linking.

Deploy Go first and migrate the store's `license_source`, `license_version` and 64 character `edition`. In Shopark's `plugins/` directory run `php shopark db:sync Saas` to add the request recovery column, then deploy PHP and APay. Reconcile existing accounts before reopening purchases. Keep the additive columns on rollback; do not revert the Go version fence or restart local Shopark billing for accounts still managed by APay.

Before launch, verify partner reachability, the active Shopark theme, sellable tagged subscription products, platform store limits, a completed reconciliation sweep, and real purchase/refund/expiry/linking flows. Builds and isolated regression tests do not establish production connectivity.

## Rollout and rollback

1. Confirm Shopark Go migration, `php shopark db:sync Saas`, and PHP deployment support explicit plan keys. Inspect the actual deployed version; a local commit is not production evidence.
2. Review APay's existing product IDs, prices, currency, metadata and references. Generate the PostgreSQL seed with `node --experimental-strip-types app/themes/shopark/scripts/generate-plan-seed.mjs`; it inserts missing products only, never deletes products or rewrites existing contracts. It assumes a CNY base-currency deployment. Review conflicting existing rows separately; retain IDs and existing subscription amounts/periods. Do not execute this SQL as an automatic deployment migration.
3. Publish APay after review. Run authenticated license-sync batches until `lastFullScan` advances after the release and `pending=0`; `503` or `failed[]/pendingRequests>0` is unfinished work. Keep the cron running.
4. With real accounts, verify purchase, upgrade, blocked active-period downgrade, refund, expiry and existing-store binding with and without downgrade acknowledgement. After expiry, verify purchase of a lower plan. Confirm currency and charge amounts before claiming payment verification.

On failure, pause new sales and restore the reviewed APay artifact/product configuration without deleting orders, subscriptions, mapping data, reconciliation state or version counters. Keep Shopark's additive schema and external billing fence. Rollback code must retain explicit `planKey` and durable versions; reverting to a client that omits the Enterprise Custom key is unsafe. To restore an entitlement, recompute the intended APay state and send a **newer** snapshot, never replay an old version. Product changes and real-account acceptance require a separate reviewed operation; this development task does not execute production SQL, charge money or publish.

Regression: `node app/themes/shopark/scripts/check-plans.mjs` (also a Shopark build check). After `npm run build`, run `node --experimental-strip-types app/themes/shopark/scripts/check-reconcile-artifact.mjs` against its temporary SQLite database and signed local partner fixture. The fixture covers partial failure 503, pending creation, restart recovery, monotonic versions, six keys, revocation/expiry, concurrency and private-state isolation. With the Shopark source and installed PHP dependencies, `php app/themes/shopark/scripts/check-partner-auth.php /path/to/shopark/plugins` runs its actual middleware against isolated in-memory RPC/Redis: shared HMAC vector, wrong signature, timestamp expiry, replayed nonce and fail-closed storage. It does not start Shopark or access its database. Run `smoke-partner.mjs` separately against a configured real Shopark for deployment verification; local contract coverage does not prove live connectivity.
