Repository navigation
1.9.79: every LTV read answers money as numbers and counts as integers - #313
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
After 82b51c8 the customer reads answered numbers and the rest of /ltv still answered mysqli's text, so a subscription's amount was 29.99 in GET /ltv/customers/{id} and "29.99000" in GET /ltv/subscriptions. Measured on a real server, the reads that still served text were: - GET /ltv/subscriptions: amount, mrr, customer_ltv - GET /ltv/breakdown by campaign, ppc_account and landing_page: every amount and rate (avg_ltv "44.562500000", repeat_rate "0.5000", spend, cac, ltv_cac) and total_orders ("4") - GET /ltv/breakdown?by=product: the amounts, units ("1.500"), orders - GET /ltv/products, PATCH /ltv/products/{id} and its delete's dry run: price - GET /ltv/companies: total_revenue, mrr, order_count, last_activity_time - GET /ltv/abm and /ltv/abm/company: total_revenue, mrr, the three engagement averages ("30.0000000"), and the company view's last_activity ("1793183200", a GREATEST() MariaDB answers as text) #305's private helper is now Prosper202\Ltv\LtvNumbers, with decimals() for money and measures and integers() for counts and unix times, and each read above goes through it where its SQL is: the repositories, and the controller for the products (whose SQL is there). Each read names every such column, not only the ones this server answers as text, since MySQL and MariaDB type COALESCE() and GREATEST() differently. integers() leaves a value with a fraction as it is, for the type tests to see, rather than cutting it. A NULL stays null (a product with no price, a subscription whose customer row is gone); a name, sku, product key or reference of digits stays a string. docs/openapi.yaml types every /ltv read with a components schema (LtvSummary, LtvAcquisitionBreakdownRow, LtvProductBreakdownRow, LtvCohort, LtvMrr, LtvPrediction, LtvProduct, LtvCompanyRollup, LtvCompany, LtvSubscription, LtvAbmAccount, LtvAbmContact, LtvField, LtvWebhook, LtvWebhookDelivery, LtvIntegration, LtvNextOffer); they were additionalProperties: true, and GET /ltv/abm's list was documented as an object. LtvDatabaseIntegrationTest holds every read on a real server: the reads are the GET routes of api/v3/index.php's /ltv group, so a route added there fails until it is held too; each answer is walked for a numeric string outside a text field; the new numbers are checked for value; and each answer has its schema's keys and JSON types, into every schema a property refers to, and the spec's 200 answer for the path names that schema. Planted, each failed a named test: the subscription amount cast, the product price cast, the company order_count cast and the ABM last_activity cast each removed; LtvSubscription.amount typed string; an undocumented key in LtvAbmAccount; GET /ltv/abm's answer untyped again; a GET route added to the group; and the whole change's code reverted to 82b51c8's. On an instance installed from 82b51c8 and then from this branch, every /ltv answer has the same values with the types above changed and no other; the Customer LTV pages (the breakdowns, ABM, subscriptions, companies, company, products, customer) are byte-identical; the agent-eval LTV cases' checks pass. p202's tables now show these amounts to the cent (9.50 where they showed 9.5) and a zero rate as 0 (it showed 0.0000), and the PHP CLI's 29.99 where it showed 29.99000. 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
Brings in ltv-customer-numbers (this branch's stacked base), and through it 1.9.79's new commits. The conflicts were in 202-config/changelogs.json, 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: N9MJ
Stacked on #305 (
1.9.79-ltv-customer-numbers, backlog RKE4): this branch starts from #305's head (82b51c8) and must merge after it. Until #305 merges, this PR's diff against1.9.79also shows #305's commit; the change here is the second commit, d58d841.What changed and why
After #305 the customer reads answered numbers and the rest of
/ltvstill answered mysqli's text, so a subscription'samountwas29.99inGET /ltv/customers/{id}and"29.99000"inGET /ltv/subscriptions(CLAUDE.md #28: a field's JSON type is part of the contract). I enumerated the reads fromapi/v3/index.php's/ltvread group (18 GET routes) and measured every answer on a real server. These still served text:GET /ltv/subscriptionsamount,mrr,customer_ltvGET /ltv/breakdownby campaign, ppc_account, landing_pagetotal_revenue,avg_ltv("44.562500000"),aov,repeat_rate("0.5000"),mrr,spend,cac,ltv_cac, andtotal_orders("4")GET /ltv/breakdown?by=productunits("1.500"),ordersGET /ltv/products,PATCH /ltv/products/{id}, its delete'sdry_runrecordpriceGET /ltv/companiestotal_revenue,mrr,order_count,last_activity_timeGET /ltv/abm,/ltv/abm/companytotal_revenue,mrr,avg_time_on_page/avg_scroll_depth/avg_video_pct("30.0000000"), and abm/company'slast_activity(aGREATEST()that MariaDB answers as text)The summary, cohorts, mrr, predict, fields, webhooks, deliveries and integrations already answered numbers.
decimals()is nowProsper202\Ltv\LtvNumbers. It hasdecimals()for money and measures, andintegers()for counts and unix times.COALESCE()/GREATEST()differently.integers()leaves a value with a fraction unchanged rather than truncating it. NULL stays null: a product with no price, a subscription whose customer row is gone. Names, skus, product keys and references made of digits stay strings.docs/openapi.yamlnow gives every/ltvread a typed components schema:LtvSummary,LtvAcquisitionBreakdownRow,LtvProductBreakdownRowLtvCohort,LtvMrr/LtvChurnInputs,LtvPrediction/LtvPredictionInputs/LtvPredictionByGroup/LtvPredictionGroupLtvProduct,LtvCompanyRollup,LtvCompany,LtvSubscriptionLtvAbmAccount,LtvAbmContact,LtvField,LtvWebhook,LtvWebhookDelivery,LtvIntegration,LtvNextOfferBefore this, all but the customer reads were
additionalProperties: true, andGET /ltv/abm's list was documented as an object.Tests: #305's type tests now cover every read
All in
tests/Ltv/LtvDatabaseIntegrationTest.php, on a real server.testEveryLtvReadServesNumbersapi/v3/index.php's/ltvgroup. A route added there fails the test until it is held to the same rule.units1.5, a null price, a company with no customer, a sku and an account name of digits that stay strings.testEveryLtvAnswerHasTheSpecsKeysAndTypes$refs. 1.9.79: the LTV customer reads answer money as numbers #305's spec reader learned{$ref: ...},items: {$ref: ...}andrequired: [...], the last so optional keys likefallback_reasonandoptions_invalidare allowed.Plants. Each plant below made a named test fail, and each was restored with a verified backup:
amountcast removedpricecast removedorder_countcast removedlast_activitycast removedLtvSubscription.amounttypedstringin the specLtvAbmAccountGET /ltv/abm's answer put back toadditionalProperties: trueAccountAdministrationIntegrationTestcompared the PATCHed price as the string'12.50000'; it now asserts12.5.Readers swept by field name (#28)
TestStructsDecodeAPIMoneyAsJSONNumber's money names now includecustomer_ltv,avg_ltv,aov,spend,cacandavg_revenue_per_customer; no struct field carries one.ltv.jsonand the administration product case read amounts withtonumber, which reads both types.@group instancesuites,tests/live/: none read/ltv.Live, before (#305 head 82b51c8) and after (this branch)
The licence stub is down. I installed with
install.php's steps without the licence form, intop202_n9mj_live, and servedphp -Son 8163 with every proxy variable pointed at the closed port 127.0.0.1:9. The licence check failed open; nothing reached the licence service. Then:tests/fixtures/agent-eval/seed.sh(Go CLI).Results:
sort_ltv.phpby campaign, ppc_account, landing_page, product and abm;ltv_subscriptions.php,ltv_companies.php,ltv_company.php,ltv_products.phpandltv_customer.php, all signed in.p202 ltv subscriptions|products|companies|abm|breakdown): values the same. An amount now shows to the cent (9.50/27.50where they showed9.5/27.5), a zerorepeat_rateshows0(was0.0000), andunitsshows2/1.50(was2.000/1.500).ltv:subscriptions|companies|abm|breakdown): the decimal text's trailing zeros are gone (29.99where they showed29.99000,13.75where they showed13.750000000).ltv.json, 6 cases) andltv-product-001, on the after instance with the reference agent: 0 fail; every case reportsneeds_judge, because without--judge-cmdthe rubric is not graded. Their mechanical checks passed.[]there, because the seed's clicks come through the direct-link tracker. The integration test covers that read with a real row.Verification
.claude/skills/p202-verify/scripts/verify.sh --since origin/1.9.79 --changed(scratch DBp202_n9mj_schema):CronEntryPointsFailLoudlyTest's "this checkout has a 202-config.php", from the live instance's config in the worktree.tests/Ltv/LtvDatabaseIntegrationTest.php: OK, 47 tests.tests/Api/V3/AccountAdministrationIntegrationTest.php: OK, 13.LtvBodyValuesIntegrationTest: OK, 10.NestedBodyValuesIntegrationTest: OK, 8.UpgradePendingAnswerServedIntegrationTestskipped itself (it needs a setup this environment lacks; it does not read/ltv).For a reviewer to check
GREATEST/COALESCEdifferently. Every count and money column is named either way, and CI's integration job runs the new tests on MySQL 8.0.MysqlCustomerCrmRepository::decimals()becameLtvNumbers::decimals(), with the same body. If 1.9.79: the LTV customer reads answer money as numbers #305 is amended, the merge may need that rename carried.MysqlCompanyRepository::listWithRollups()'s docblock was stranded abovesearch(); I moved it ontolistWithRollups().recordtoLtvProduct/LtvCompany.git merge-treeagainst 1.9.79: LTV webhooks go through the export webhooks' guard and sender #278, 1.9.79: a dead LTV webhook can be revived, and is sent what it missed in the last 7 days #289 and 1.9.79: LTV Settings reads the row a button names exactly #296 (and also 1.9.79: the minutely cron sends LTV webhooks, within a budget #273, 1.9.79: the cron job sweeps lapsed LTV subscriptions hourly and runs the rest of LTV maintenance daily #285, 1.9.79: the subscription sweep announces what it writes, for the last week #302, 1.9.79: company linking walks past names it cannot link #304, 1.9.79: Customer LTV's pages read the record a button names exactly #308) conflicts only inchangelogs.txtand202-config/changelogs.json. None of them changes a read's columns.Generated by Claude Code