# Commerce analytics core `commerce` is the product-centric analytics boundary of EUSRR. It does not replace an operational CRM or warehouse. External and future internal systems all supply records to the same canonical product model. ## Stable model - `Product` is one sellable or stockable SKU. `parent` groups variants. - `SourceRecord` retains the latest raw payload with source provenance. - `ProductBinding` maps a source product/offer identifier to `Product`. - `Sale` and `SaleLine` contain normalized product-level sales facts. - `Cost` contains commissions, delivery, advertising and other expenses. - `StockSnapshot` contains point-in-time stock observations by source and warehouse. Money is never aggregated across currencies. Returns are represented as a `Sale` with `event_type="return"`; line values remain positive and analytics applies the sign. ## Integration contract An adapter subclasses `commerce.integrations.SourceAdapter`, handles API pagination internally and yields `ExternalRecord` objects. Register it in `adapter_registry`, then invoke the Celery task `commerce.sync_source`. Adapters must not store credentials in `SourceSystem.metadata`; secrets belong in environment-backed configuration or a dedicated encrypted credential store. `IntegrationConnection` is that encrypted credential boundary for connections created by employees. `moysklad` scans the complete paginated assortment, aggregate stock, shipments and customer returns. `wildberries` reads the current Finance API realization details and normalizes sales, returns, commissions, acquiring, logistics, storage and deductions. Raw API payloads are retained in `SourceRecord` before normalization. Repeated scans update the same source identities and append stock observations. Wildberries imports save the last `rrdId` after every non-empty page and continue on the next scheduled run until the API returns HTTP 204, as required by the provider contract. This avoids both duplicates and a worker sleep under the provider's one-request-per-minute Finance API limit. After the initial history is complete, each run rechecks the latest 30 days so report corrections replace prior facts. Celery Beat checks due connections every minute. Each connection stores whether automatic synchronization is enabled, its interval (15 minutes by default) and the next planned run; row locking and connection status prevent overlapping full scans. Both a Celery worker and Celery Beat must be running. Imported cards are matched by an existing source binding, SKU or barcode. A card that cannot be matched safely is created separately and can later be merged into a canonical product through `products/{id}/merge/`; bindings and normalized facts move with it. Source-specific code must not create alternative product models. It stores the raw payload first and then resolves a product through `ProductBinding` or `commerce.services.match_product` before writing normalized facts. ## API The authenticated API is mounted at `/api/v1/commerce/`: - `products/` - `products/{id}/analytics/` - `products/{id}/merge/` - `connections/` (`test/` and `sync/` actions) - `sync-runs/` - `sources/` - `product-bindings/` - `sales/` - `sale-lines/` - `costs/` - `stock-snapshots/` All employees may read. Writes require staff status or the corresponding Django model permission. The frontend management screen can bootstrap and correct normalized records manually; integrations should still write them through application services so raw provenance and idempotency are preserved. ## Local project management Local project cards, teams and daily participation live in the sibling [`projects`](../projects/README.md) app. `ProjectExternalBinding` explicitly maps source object identities to local projects. Commerce synchronization preserves external projects and payments and never overwrites local project management data. Active external projects without a binding appear in the `/projects` intake queue, where a manager can create or select their local project explicitly. Company analytics groups imported sales, costs and project payments by these exact bindings. Unbound source records remain visible as a separate reconciliation group.