Repository navigation
1.9.79: the LTV customer reads answer money as numbers - #305
Merged
Merged
Conversation
GET /ltv/customers/{id} answered mysqli's DECIMAL strings beside integer
ids: "total_revenue": "54.12500", "mrr": "0.00000", each subscription's
amount and mrr, each revenue event's amount, and each line item's
quantity ("1.500"), unit_price and amount. GET /ltv/customers did the
same for the rollups, and the engagement read answered SUM()'s "1" for a
campaign's conversions and "75.000" for an event's value, while
GET /ltv/summary has answered numbers since 17305ee: one field, two types,
depending on the endpoint read.
The casts are where 17305ee put the summary's, in the repositories, so
every reader gets the same types: MysqlLtvRepository::customers(),
MysqlCustomerCrmRepository::get() (a NULL unit_price stays null; quantity
is a number, DECIMAL(12,3), 1.5 as well as 2) and
MysqlEngagementRepository's customer reads. The pages that read them
(the customer modal, the customer table, the download, the merge search)
already cast; their output is byte-identical before and after on a seeded
instance. Every write that answers a customer (POST/PATCH, merge, alias)
and the erasure's dry_run record answer the same, through customerDetail()
and crm->get(). next-offer already answered numbers.
The same endpoint answered custom_fields as {} with a value and [] with
none (PHP's empty array): it is an object either way now, at the API edge,
since the page reads it as an array.
docs/openapi.yaml types the answers (LtvCustomer, LtvCustomerDetail and
its parts, LtvCustomerEngagement). LtvDatabaseIntegrationTest holds them on
a real server: every customer read and the summary walked whole for a
numeric string outside a text field (a DECIMAL column added later is seen
too), the values exact, and each answer's keys and JSON types equal to
the spec's schema. Each defect was planted and caught. The Go CLI decodes
no struct from these answers; its money floor now names refunded_amount,
mrr and unit_price, and the --all fake answers numbers as the server does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Contributor
Contributor
Brings in 1.9.79's new commits. The conflicts were in 202-config/changelogs.json, changelogs.txt, where both sides added lines: the incoming lines are kept as they are and this branch's follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backlog: RKE4
What changed and why
GET /ltv/customers/{id}answered mysqli's DECIMAL strings beside integer ids, whileGET /ltv/summary(and the rest of the API, since 17305ee) answers money as JSON numbers: one field, two types, depending on the endpoint an agent read. Measured on an instance built fromorigin/1.9.79and seeded with a customer holding two sales with line items, a refund, two subscriptions and engagement:origin/1.9.79)data.total_revenue/refunded_amount/mrr"54.12500"/"5.00000"/"39.99000"54.125/5/39.99data.subscriptions[].amount/mrr"120.00000"/"10.00000"120/10data.recent_events[].amount"-5.00000"-5data.recent_events[].items[].quantity/unit_price/amount"1.500"/"24.50000"(or null) /"10.12500"1.5/24.5(null stays null) /10.125GET /ltv/customersrows:total_revenue/refunded_amount/mrrGET /ltv/customers/{id}/engagement:browsing[].conversions/events[].event_value"1"/"12.500"1/ number12.5(null stays null)GET /ltv/customers/{id}/next-offerdata.custom_fieldsof a customer with no value[](a list;{...}with a value){}Counts other than
conversions(order_count,active_subscription_count,billing_interval_count,clicks) and every id were already integers (mysqlnd's binary protocol types integer columns) and stay so; text that may be all digits (primary_ref,postal_code,phone,alias_value,external_sub_id,transaction_id,sku) stays a string.Where the casts are. 17305ee has no shared helper: it cast the LTV summary inside
MysqlLtvRepository::summary()("so the Customer LTV page reads the same"), the CRUD base innumericDecimals(), and the click reads at the edge. These reads follow the summary, in the same repositories, so every reader gets one type:MysqlLtvRepository::customers()(the summary's loop),MysqlCustomerCrmRepository::get()(adecimals()helper with 17305ee's edge rule,is_string && is_numeric-> float, so a NULLunit_pricestays null) andMysqlEngagementRepository::customerEvents()/customerEngagement(). That also covers every write that answers a customer (POST and PATCH/ltv/customers, merge, alias, all throughcustomerDetail()) andDELETE /ltv/customers/{id}?dry_run=1'srecord.LtvControllerchanges onlycustomerDetail()and the erasure preview, forcustom_fields(below); the webhook code the three open LTV branches change is untouched.custom_fieldswas{...}for a customer with a value and[]without (PHP's empty array). It is converted at the API edge (LtvController::presentCustomer()), not in the repository, because the customer page reads it as an array.Readers swept by field name (CLAUDE.md #28).
ltv_customer.phpmodal,sort_ltv.phptable,ltv_download.phpexport,ltv_merge_search.php): all cast already. Captured signed in on the instance before and after: byte-identical (tokens masked), and they show the seeded values ($54.13,×1.50,75%).ltv customers,ltv engagementrender generically;--allre-encodes rows).TestStructsDecodeAPIMoneyAsJSONNumbernow also namesrefunded_amount,mrrandunit_price; the--allfake sent"total_revenue":"10"(the old contract) and now sends numbers, with an assertion that the amount survives the re-encode as a number. Tables: the list showed54.125before and after; the detail's key-value view showed39.99000, now39.99;--csvwrites39.99where it wrote39.99000.ltv:customers,ltv:engagement): genericFormatter, casts to string for display.p202 mcpand the hosted engine): the catalog has no LTV tool, so no LTV reader there.tests/fixtures/agent-eval/): the ltv cases read amounts through jqtonumber, which takes a number or a numeric string; nothing to change.@group instancesuites andtests/live/: none reads an LTV customer.EngagementEventTest, noevent_value) now sends it as the server does and asserts the number.Docs.
docs/openapi.yamlhadadditionalProperties: truefor these answers (no field typed as string, nor as anything else); it now types them:LtvCustomer,LtvCustomerDetail(allOfLtvCustomer) withLtvCustomerAlias,LtvCustomerSubscription,LtvCustomerRevenueEvent,LtvCustomerLineItem, andLtvCustomerEngagementwithLtvBrowsedCampaign,LtvEngagementEvent.documentation/api/20-ltv.mdgains a "Numbers" paragraph. Both changelogs: aChanged:line.Tests
tests/Ltv/LtvDatabaseIntegrationTest.php(@group integration, real server: the types are the server's, which a fake connection would only repeat), extending the file's existing "numbers, not DECIMAL strings" cohort check rather than adding a parallel one:testCustomerReadsServeNumbers: summary, list, detail, engagement, next-offer and the erasure preview throughLtvController, each walked whole for a numeric string outside a listed text field (so a DECIMAL column added later is seen), plus exact values (54.125,1.5, a nullunit_price, a nullevent_value,conversions1) and digit-only text staying text. This also holds the summary 17305ee changed, which had no test.testCustomerAnswersHaveTheSpecsKeysAndTypes: each answer's keys equal the OpenAPI schema's properties, and each value's JSON type is the one the schema states (a list or an object judged byjson_encode, so[]for an object fails).Planted and caught (each restored and
cmp-checked): list rollups uncast; summary amounts uncast; detail customer row, subscriptions, events, line-item quantity each uncast; a NULLunit_pricecast to 0;conversionsuncast;event_valueuncast; the spec statingmrras string, leaving a served key out, listing a key not served,unit_pricenot nullable; thecustom_fields{}conversion removed. Go: the--allfake sending a string, and a struct decodingmrrinto a string, each fail their test.Verification
.claude/skills/p202-verify/scripts/verify.sh --since origin/1.9.79 --changed, on the committed branch, with a scratch MariaDB 10.11 database for the schema tier and no202-config.phpin the checkout (so the cron entry-point test ran rather than skipping):phpcs: 6 files examined, 0 made worse. unit: 6104 tests, 4 skipped (the documented "User class requires DB singleton" skips). The
--changedrun on the working tree before committing reported phpcs FAIL (two test files over HEAD's warning and error counts); the new lines were wrapped and the run above is after that.Also run, outside the ladder:
LtvDatabaseIntegrationTest(45 tests),LtvBodyValuesIntegrationTest(10),NestedBodyValuesIntegrationTest(8),ForeignRecordNamesIntegrationTest(10),ReClickCountsOnceIntegrationTest(3),LtvProductPredictionIntegrationTest(6): all OK.php -Son 8153): the four endpoints' JSON types before (onorigin/1.9.79's code) and after, as in the table above; the pages before/after;p202 ltv customers,ltv customers <id>,ltv engagement <id>,--csv,--all --jsonthrough the built Go CLI.tests/fixtures/agent-eval/cases/ltv.json, 6 cases, reference agent, no judge): before, on an instance oforigin/1.9.79's code seeded for the first time, 6/6needs_judge(their deterministic checks pass); after, on a fresh instance of the final branch, 6/6needs_judge, the same command counts.What could not run, and how the instance ran without the licence
The licence stub is down. The instance was installed without the licence form:
install.phprefuses an unverified key server-side, so the schema, the account and its API key were made by the installer's own classes from a script (INSTALL::install_databases(),InstallAccount::create(), anINSERTinto202_api_keys, partitions), not through the web form. It was then served with outbound HTTP(S) pointed at a dead proxy, so every licence check got no answer and failed open as designed (nothing reached my.tracking202.com): with that, sign-in to the pages and the Go CLI both worked, so the live CLI path did run (p202 ltv ...and the eval cases through it). Not exercised: the licence gate itself, and the web installer's licence step.No judge ran for the eval rubrics (
needs_judge): only their deterministic halves were checked.For the reviewer
git merge-treeagainst 1.9.79: LTV webhooks go through the export webhooks' guard and sender #278 (1.9.79-ltv-webhook-guard), 1.9.79: a dead LTV webhook can be revived, and is sent what it missed in the last 7 days #289 (1.9.79-ltv-webhook-revive) and 1.9.79: LTV Settings reads the row a button names exactly #296 (1.9.79-ltv-delete-webhook-id): onlychangelogs.txtand202-config/changelogs.jsonconflict (every 1.9.79 PR appends there);LtvController.php,docs/openapi.yamlanddocumentation/api/20-ltv.mdmerge cleanly.quantityis DECIMAL(12,3) and the API accepts1.5, so it is a number, not an integer.GET /ltv/subscriptions(amount,mrr,customer_ltv) andGET /ltv/breakdown?by=product(total_revenue,orders,units,aov,mrr,repeat_rate,avg_revenue_per_customer); by reading (fetchAll()with no cast),/ltv/breakdownby campaign, traffic source account and landing page,/ltv/products(price),/ltv/companies,/ltv/abmand/ltv/abm/company(whose contacts carry the customer fieldsorder_count,total_revenue,mrr). After this PRGET /ltv/subscriptionsanswers a subscription'samount/mrras strings while the customer detail's embedded subscriptions answer numbers. I left them for a follow-up item rather than widen this one.Generated by Claude Code