Skip to content

External writes

product.stock is an output. Stockly derives it from the shelves after every operation and overwrites whatever it finds there. An integration that writes it is not talking to the warehouse; it is racing the next recalculation, and losing.

This page is the mechanism. The setting, the register screen and the daily summary are described in external stock writes for the people who operate them.

Every legitimate write updates product.stock and an availability shadow in the same transaction. Because the pair moves atomically, a mismatch observed under the caller’s row lock cannot be an intermediate state, because nothing can be halfway through. Somebody wrote the field without telling the module.

The shadow is maintained whether or not detection is switched on, so turning detection on later starts from a current baseline rather than from a cliff of false positives.

Two detection points, with different timing:

Source valueWritten byCaught
dalthe Admin API, the admin UI, any DAL writeimmediately, from the product write
sqlraw SQL straight into the databaselazily, at the product’s next stock event — exactly the moment the module would otherwise silently overwrite it

The threat model is a misconfigured integration, not sabotage from inside the database. A foreign system that also forges the shadow table is out of scope for any application-level mechanism.

The policy is a setting, and its values reach the integration verbatim in policyApplied:

ValueWhat happens to the number
offno detection and no record: a DAL edit is absorbed as an ordinary stock correction, a raw-SQL write is overwritten by the next recalculation
correctthe external figure wins — the difference is booked into the warehouse and the incident recorded
restorethe shelves win — nothing is booked, the field is rewritten from the shelves, and the rejected write is recorded

Under correct the difference lands as a real movement, so the correction is in the product’s history like any other. Under restore there is no movement, because nothing physically changed.

p2lab_stockly.external_stock_write_detected fires once per product per unit of work, after the commit:

Key
productId
expectedValuewhat Stockly held
foundValuewhat the writer left behind
deltathe drift
sourcedal or sql
policyAppliedoff, correct or restore

Unlike every other event in the catalogue, this one carries scalar values rather than the shared envelope, because it is written for the Flow Builder and for App System webhooks, which is also why a merchant can wire it to a mail without anybody writing code.

p2lab_stockly_external_stock_incident, one row per detection:

Field
productId
expectedAtpthe figure the shelves add up to
foundValuethe figure somebody wrote
delta
sourcedal or sql
policyAppliedcorrect or restore
userId, integrationIdwho wrote it, when the write came through the API

integrationId is the useful one for an integration author: it names the API integration that made the write, which turns a vague report of stock being overwritten into a specific credential.

Read it like any entity (/api/search/p2lab-stockly-external-stock-incident); a rising count for one integrationId is worth an alert of its own.

Rows survive the product being deleted, on purpose: an audit trail that disappears with its subject is not an audit trail. They are purged after a year.

You want toCall
book goods inwarehouse/stock/add
book goods outwarehouse/stock/remove
correct a figure to a counted onewarehouse/stock/count
repair a product.stock somebody already overwrotewarehouse/stock/sync-product/{productId}

Each of those leaves a ledger row, recalculates the published figure, and updates the shadow, which is the whole difference between a stock change and a stock change nobody can explain later.

If an external system genuinely owns the numbers, that is a legitimate architecture: set the policy to correct and let it win explicitly, with every override recorded. What is never right is writing the field with detection off and assuming it stuck.