Jewelry AI, how a purchase is resolved
The store's notice is a lookup key, not the authority on what you receive
When a purchase completes, the message the store sends does not decide what it turns into. It carries an identifier, and that identifier is looked up in our own product records, which is where the app it belongs to and the grant it produces are held. When the lookup does not resolve to exactly one active record, the grant is stopped rather than approximated.
Two records are involved, and only one of them decides
A completed purchase produces a notice from the store. That notice is read for what it can be trusted to carry: the store product identifier, and the policy fields that describe the event. The identifier is then used to find a product record in our own database, and that record is what says which app the purchase belongs to and what grant it produces (apps/api/src/webhooks/revenuecat.service.ts, source tree read 2026-08-18).
This is worth stating plainly because the intuitive model is the reverse. The purchase happened at the store, so the store's message feels authoritative. In the code it is an input, and the authority is a row we maintain.
One conclusion does not follow from this arrangement, and it should not be drawn: nothing here demonstrates that the store or the provider sends a correct amount. The design avoids depending on that question rather than answering it.
What the notice contributes, and what the product record decides
The split is the whole mechanism. Everything else on this page follows from where a given question is answered.
| Question | Answered by |
|---|---|
| Which store product is this about | The identifier carried by the notice, used as a lookup key |
| Which app does the purchase belong to | The product record in our own database |
| What grant does the purchase produce | The product record in our own database |
| How large is that grant | The product record; the notice is not read for it |
| Which environment did the purchase come from | The notice, read as part of the resolution |
Conditions that stop a grant rather than guess at it
Each of these is a refusal to act on an unresolved lookup. They are described here by what each one does; this page does not claim the order in which they run.
- The identifier resolves into more than one app.
- The event is treated as ambiguous and held for internal review. Retrying is pointless, because the same lookup would produce the same ambiguity (apps/api/src/webhooks/revenuecat.service.ts).
- Identifiers being unique across apps is operational discipline.
- This is a catalogue hygiene trap rather than a guarantee enforced by code. The check detects the collision; it does not prevent one from being recorded.
- The product record is inactive.
- A purchase of a retired product does not produce credit and is held for internal review instead.
- The purchase came from the store's test environment.
- In a production setup the grant path is closed by default and opens only on an explicit declaration. While closed, the event reaches a definite ignored outcome rather than being retried indefinitely. What the setting is in production cannot be read from the code.
Why a retired product is still found, and why that is deliberate
Retiring a product deactivates the record; it does not remove it. The lookup therefore still finds it, and the refusal happens later, on the path that would grant credit (apps/api/src/webhooks/revenuecat.service.ts).
That produces an asymmetry which is easy to mistake for an inconsistency. A purchase of a retired product grants nothing and is held for internal review, while a refund or an expiry event for that same retired product is processed normally. The reason is direct: if retirement blocked every path, credit that had already been granted could never be reclaimed afterwards.
The environment gate has the same shape. It closes the granting path only. On the refund side the environment is part of the lookup key, so a refund raised in the test environment does not find a real purchase to act on in the first place.
What each situation resolves to
Read the middle column as what the system does, not as an action available to a reader. Internal review is an internal outcome.
| Situation | Choose | Why |
|---|---|---|
| The identifier resolves to one active product in one app. | The grant is produced from that record. | The record is the authority on which app and which grant the purchase corresponds to. |
| The identifier is defined in more than one app. | The event is stopped and held for internal review. | The lookup is ambiguous and a retry cannot resolve an ambiguity that is present in the records themselves. |
| The identifier resolves to a retired product, and the event is a purchase. | No credit is produced; the event is held for internal review. | Retirement deactivates the record, and the gate sits on the granting path. |
| The identifier resolves to a retired product, and the event is a refund or an expiry. | It is processed as normal. | Blocking those paths would leave previously granted credit impossible to reclaim. |
| The purchase came from the store's test environment, in a production setup. | No real credit is written, and the event finishes as ignored. | The grant path is closed unless it is opened by an explicit declaration, and a terminal outcome is preferable to indefinite retrying. |
What this page does not cover
- Amounts, prices, pack or plan names and currency. The only statement supported here is that the size of a grant comes from the product record.
- The order in which the checks described above are applied. Each one is described by what it does, not by its position in a sequence.
- What happens when the same notice arrives more than once, or when notices arrive out of order. No exactly-once behaviour is claimed on this page.
- The value of the test-environment setting in production. The default was read from the code; the deployed configuration was not.
- The names of configuration flags, environment variables or any verification parameter. Deliberately outside the reading behind this page.
- How the store or the provider decides what to send, and on what schedule it retries. The code assumes an interface; it does not prove the other side's behaviour.
- How credit is spent once it exists, and how a balance is computed. That belongs to a different part of the system.
- Whether the logic described here is the version currently deployed. What was read is the source tree as it stands in this worktree on 2026-08-18.
Questions
Does the store's message decide how much credit a purchase produces?
No. The notice is read for the store product identifier and the policy fields it carries, and the identifier is then looked up in our own product records. That record holds the app the product belongs to and the grant it produces (apps/api/src/webhooks/revenuecat.service.ts, source tree read 2026-08-18). Nothing in this arrangement demonstrates that the sending side reports a correct amount; it is built so that the question does not arise.
What happens if the same store product identifier is recorded for more than one app?
The lookup returns more than one match, the event is treated as ambiguous and it is held for internal review rather than retried, because a retry would encounter the same ambiguity. Keeping identifiers unique across apps is operational discipline in the catalogue; it is not something the code enforces, and the check detects the collision rather than preventing it.
Why can a purchase of a discontinued product produce nothing while its refund still goes through?
Retiring a product deactivates the record instead of deleting it, so the lookup still finds it, and the gate sits only on the path that grants credit: a purchase produces no credit and is held for internal review, while refund and expiry events for the same product are processed normally (apps/api/src/webhooks/revenuecat.service.ts). The asymmetry is deliberate, because blocking every path would make previously granted credit impossible to reclaim.
Does a purchase made in the store's test environment produce real credit?
Not by default in a production setup. The granting path is closed for test-environment purchases and opens only on an explicit declaration, and while it is closed the event reaches a definite ignored outcome rather than being retried indefinitely. The gate applies to granting only; on the refund side the environment forms part of the lookup key, so a test refund does not find a real purchase to act on. What that setting is in production cannot be read from the code and is not stated here.