Repository navigation
1.9.79: JVZoo's JVZIPN at jvzoo.php, for affiliates and vendors (stacked on #268) - #272
Merged
Merged
Conversation
Account › API integrations handed out tracking202/static/paykickstart.php as PayKickstart's Affiliate IPN URL under "No setup needed", and GET /system/integrations and `p202 system integrations` listed it. The file never existed, so every IPN sent there was a 404. The Affiliate IPN could not have worked anyway: it is unsigned JSON with PayKickstart's own tracking id and no custom variables, so nothing in it names a click. The URL now takes PayKickstart's product (vendor) IPN. A vendor puts subid=[[subid]] on the checkout link that is the campaign's offer URL, pastes the URL into each product's IPN Integration tab and stores each PayKickstart campaign's IPN secret key (202_users_pref, one per line, at most 20, never served back). Each IPN is verified against every account's stored keys with PayKickstart's published HMAC-SHA1 algorithm before anything in it is read, and is recorded for the account whose key verifies it, on a click of that account (another account's click reads as no click). sales records a conversion (source paykickstart, pixel type 2) under its transaction_id at its amount; refund reverses it; test mode, subscription-payment and every other event record nothing. A retry under a transaction id on file is a duplicate only when it states the same amount, and is refused (409) when it states another. Every outcome is a status PayKickstart's log shows, with the reason, and one log line without a key or buyer data. The receiving side is provider-neutral (Prosper202\PaymentNotification: a ProviderAdapter that verifies and parses, one Processor that records or reverses through p202RecordLegacyConversion(), one Response mapping), so JVZoo and ClickBank can follow as adapters. HandedOutUrlsNameServedFilesTest holds every script a served literal names, and every integration GET /system/integrations lists, to a file in the tree; jvzoo.php, zpn.php and slack.php are listed as known dead until their own changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
202-config/changelogs.json kept both: #258's updated lines, then the PayKickstart line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
#258 made the API answer a 5xx while the files are deployed and the upgrade not run with a 503 naming the upgrade. The IPN endpoint is not in the API's tree, and between those two moments its keys' column is not there yet: it answered 500. The Receiver now asks SchemaVersion::pendingUpgrade() on a failure and answers 503 with its sentence (OutcomeKind::UPGRADE_PENDING); PayKickstart's retry lands once the upgrade has run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…ey is "the key" admin-005 asks an agent to store a campaign's PayKickstart IPN secret key and give the URL to paste: the key is stored with user prefs update, the URL is the one system integrations answers, and neither the preferences read nor the reply repeats the key. The reference agent answers it; an agent that stores nothing and repeats the key fails all four checks. Search puts system integrations first for the ask (two tasks in one ask), so it is listed in searchKnownMisses rather than taught by a phrase lifted from the ask. A refusal of a single pasted key says "The key is 5 characters long" rather than "Key 1"; a list still names the key by its place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…what the reversal on file took back A refund with no amount (or 0) takes back the whole sale. Its comparison with the reversal on file was skipped when the amount was null, so a full refund after a partial one was answered 200 duplicate: PayKickstart stopped retrying while the rest of the sale stayed credited (CLAUDE.md #15). The Processor now reads the reversal on file with the sale it reverses and compares what the refund takes back, a full refund read as the sale's amount. Any other refund of a sale already reversed is a 409 naming both: "already reversed by 10.5, and 36.5 of its 47 is still credited; this refund states its full amount, 47". A sale is still reversed once. The other duplicate decision, a sale or rebill under a transaction id on file, compares two amounts that are never null (the sale's is required, the stored one NOT NULL). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Account › API integrations, GET /system/integrations and p202 system integrations handed out tracking202/static/jvzoo.php as JVZoo's IPN URL, and the file never existed: every JVZIPN sent there was a 404. It now runs the payment-notification Receiver (#268) with a JVZoo adapter. JVZoo's own article ("JVZIPN - How to create your own integrated script", read through its Help Center API) is the source. The cverify is its published validator's: every posted value but cverify's, in field-name order, each followed by "|", the account's JVZIPN secret key, SHA-1, the first 8 hex digits upper-cased; compared with hash_equals(), where the sample's == would take "+12345678" for an all-digit cverify. It is checked against every account's stored key before anything is read, and the account is the one whose key verifies it (CLAUDE.md #16), so a vendor's sale that names no click is still acknowledged. A JVZIPN carries two click fields, caffitid (the tid on an affiliate's link) and cvendthru (the variables on the vendor's buy link), and each is the other party's choice to the one who did not set it. JVZoo asks for the URL in two places, an affiliate's Affiliate IPN and a product's JVZIPN v1 URL, so each gets its own URL: ?role=affiliate reads only caffitid, ?role=vendor only cvendthru's subid; a URL with no role is a 400. SALE and BILL record a conversion (source jvzoo, new) under ctransreceipt at ctransamount; RFND, CGBK and INSF (JVZoo: an eCheck chargeback, so a reversal, not nothing) reverse the whole sale ctransreceipt names, since JVZoo refunds a payment whole; CANCEL-REBILL, UNCANCEL-REBILL and TEST record nothing. JVZoo marks no test notification: a Test Purchase Mode sale is a real sale at a test price. ctransamount must have a decimal point: the field table says pennies and the notifications say dollars. Only the fields JVZIPN v1 documents are taken, because the cverify joins values without names and an added field moves every later value along (a receipt read from ctranstime would record the sale twice). JVZIPN v2, whose cverify covers neither the amount nor the tid, is a 415. One change to #268's Processor: a sale already on file that a live reversal nets (JVZoo sends RFND, then SALE again, when a refund is undone) is a 409 naming the reversal to delete, where it was answered 200 duplicate with the sale still reversed. The key is read and written through JVZooKey (trimmed; a line break or a control character refused) by the page, PUT /users/{id}/preferences and the endpoint, so GET /system/integrations secret_stored says whether a key the endpoint can sign with is stored. The panel gives both sets of steps. 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. |
…er a deleted reversal reverses again A sale stated again under a transaction id whose sale a refund took back was answered 200 duplicate while the sale stayed reversed. JVZoo sends an undone refund as RFND and then SALE again; the sale is now a 409 naming the reversal to delete to count it (ported from the JVZoo branch). Deleting that reversal did not make a later refund work: a deleted row keeps its dedupe key, every refund without an id of its own shared rev:<sale>:1, so the next refund of the sale found the deleted row and was answered 200 duplicate with nothing reversed. Such a refund is now keyed by its body, so only the same notification sent again unchanged finds a reversal already on file, live or deleted; any other is either compared with the reversal that stands (the writer refuses a second one under its lock, and the Processor answers duplicate when it takes back the same) or, when none stands, reverses the sale again. A refund with its own id is still told apart by that id. The 403 names the provider's signature field (ProviderAdapter:: signatureField(); PayKickstart's is hash). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
changelogs.json: 1.9.79's lines, then the PayKickstart line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…JVZoo branch #268 ported the reinstatement 409 from this branch, keyed a refund with no id of its own by its body (so a refund after a deleted reversal reverses again), named the signature field in the 403 through a new ProviderAdapter::signatureField(), and merged 1.9.79 (#262). changelogs.json conflicted: 1.9.79's lines are kept, the JVZoo line after them. The JVZoo adapter gains signatureField() ("cverify") in this commit, since without it the class no longer satisfies the interface. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…ppened p202RecordLegacyConversion() dated every row time(), and the Processor dropped the time a notification states, so an IPN PayKickstart delivered days late (it retries a failed delivery up to 13 times) was filed on the day it arrived in every view by conversion time: the conversion list's time filter, attribution, the click-to-conversion time. The recorder now takes a conv_time, and the Processor passes the stated time, held between the earliest it can be (the click a sale credits; for a refund, the sale it takes back, so the sale keeps its own day) and its arrival (a clock ahead of this server's). The answer and the log line say when a time was held. With no time stated, the row is dated when it arrives, as before. transaction_time is signed (every non-empty field is); one that is not a Unix time is now a 400 naming it, where it was read as no time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…start IPN branch
GET /users/{id}/preferences now serves every integration secret as its
stored state (#270), and the PayKickstart IPN keys are one more:
paykickstart_ipn_secret_keys is on ServedPreferences::SECRET, answered
{"stored": true}, and the count this branch served in its place
(paykickstart_ipn_keys_stored, PayKickstartKeys::withoutKeys()) is gone.
GET /system/integrations keys_stored still says how many. The
PreferenceSecretsAreNotServedIntegrationTest round trip stores the keys
with the other secrets.
Conflicts: changelogs (1.9.79's lines, then this branch's), the
preferences docs and the OpenAPI schemas (1.9.79's, with the keys added
as a StoredSecret), and the controller (1.9.79's).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…ending The page's Remove form named a key by its place and the four characters it shows (…AB12). Another tab could remove that key and add a different one ending in the same four characters at its place; the first tab's form then matched the new key and removed it (measured on the pushed class: removed(['campaign-new-AB12', …], '1', '…AB12') dropped campaign-new-AB12). The form now carries an HMAC-SHA256 of the whole key under a secret this session holds server-side (PayKickstartKeys::removalToken()): the page shows nothing a reader could match a guess against, and a form made for one key cannot match another. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…ersal reverses again With #268's body-keyed refunds, a JVZoo refund sent after the reversal was deleted reverses the sale again, and the first refund sent again unchanged is a retry. The integration test now walks that path (SALE, RFND, SALE 409, delete, the RFND again, a later RFND) and pins the 403's wording to cverify; the unit test pins signatureField(). The guide and the changelog say what a refund does after the reversal is deleted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…JVZoo branch #268 now dates a notification's row by the provider's own time, held to the click for a sale and to the sale for a refund, and brings 1.9.79's #270 (ServedPreferences: GET preferences serves each secret as {"stored": ...}), #280, #281 and #282. Conflicts: both changelogs keep the base's lines with the JVZoo line after them; the OpenAPI preferences update takes #270's oneOf shape, with the JVZoo key's rule in its string branch; the eval cases keep #268's admin-005 notes and add admin-006 after it. jvzoo_ipn_secret_key is already on ServedPreferences::SECRET, and JVZoo adds no other column. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
#268's Processor now dates a row by the provider's own time and holds it to the click (a sale) or the sale (a refund) and to arrival. JVZoo's ctranstime is that time: it was read leniently and only logged, so an unreadable one became "no time" and the row was dated when it arrived. It is now a Unix time of 1 to 10 digits, at most Notification::LAST_TIME, or the notification is refused with a 400 naming ctranstime; absent or empty, the row is dated when it arrives. JVZooIpnIntegrationTest gains a late notification: a sale three days late and its refund two days late keep their own times, a chargeback stating a time before its sale is dated at the sale, a ctranstime changed after signing is a 403, and one that is not a Unix time is a 400. The guide and the changelog say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…JVZoo branch #268's stale-Remove fix for PayKickstart's keys (PayKickstartKeys and its panel); it merged cleanly beside the JVZoo panel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Contributor
This was referenced Oct 28, 2026
Brings in 1.9.79's new commits. The conflicts were in 202-config/changelogs.json, 202-config/functions-upgrade.php, 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 paykickstart-ipn (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
PayKickstartAdapter::storedSecrets() read every 202_users_pref row, and a
deleted account keeps its row and the keys in it. Its key kept verifying
IPNs, so a sale on a deleted account's click was recorded for it, and a key
the deleted account shared with a live one verified both. ClickBank's and
JVZoo's adapters, on the branches above, read their keys the same way, and
ClickBank refuses a key two accounts store with a 409: a deleted account
holding a live account's ClickBank key refused every notification of the
live account, with nothing in the UI to clear the deleted copy.
AccountSecrets::stored() is now the one read of a provider's secret column
that decides the accounts a notification can belong to: it joins
202_users and leaves out user_deleted = 1. A user switched off in Account ›
Users (user_active = 0) still owns its keys: a sale PayKickstart reports
for that account is not that person acting. ProviderAdapter documents it.
StoredSecretsReadThroughAccountSecretsTest holds every ProviderAdapter's
storedSecrets() to a call of AccountSecrets::stored() with no statement of
its own naming 202_users_pref, and every other statement in
202-config/PaymentNotification and the payment endpoints that names
202_users_pref to a listed reason (the Processor's currency read, for the
account already decided).
Tests: PayKickstartIpnIntegrationTest's new
testADeletedAccountsStoredKeyVerifiesNothing seeds a deleted account that
stores a live account's key and one of its own: the live account's sale is
recorded for it, the deleted account's click is a 404 and its own key a
403, and a switched-off account's sale is recorded. Planted: the
user_deleted filter removed from AccountSecrets fails it ("Recorded
transaction PK-TX-D2 on click 99040131"), and so does a user_active = 1
filter added (the switched-off account's IPN unverified); storedSecrets()
reading 202_users_pref itself fails both structural tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Brings in #268's AccountSecrets: a deleted account's stored keys verify nothing. The conflict was in changelogs.txt, where both sides changed lines of 1.9.79: #268's PayKickstart line is kept as it is and this branch's JVZoo line follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
JVZooAdapter::storedSecrets() read jvzoo_ipn_secret_key from every 202_users_pref row, and a deleted account keeps its row and its key. Its key kept verifying JVZIPN notifications: a sale on a deleted account's click was recorded for it, and a key it shared with a live account verified both. storedSecrets() reads the key through #268's AccountSecrets::stored(), which leaves out deleted accounts; a switched-off account (user_active = 0) still owns its key. StoredSecretsReadThroughAccountSecretsTest, merged from #268, now finds JVZooAdapter and holds it to that read (it failed on the merge until this change). Tests: JVZooIpnIntegrationTest's new testADeletedAccountsStoredKeyVerifiesNothing: with a deleted account storing the account's key, the account's sale is recorded for it; the deleted account's click is a 404, a key only it stores a 403; a switched-off account's sale is recorded. Planted: storedSecrets() reading 202_users_pref itself, and AccountSecrets without its user_deleted filter, each fail it ("Recorded transaction D2 on click 99050141"); a user_active = 1 filter fails it with a 403. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
tracking202
added a commit
that referenced
this pull request
Oct 28, 2026
tracking202
added a commit
that referenced
this pull request
Oct 28, 2026
…ike the others #272's JVZooAdapter read 202_users_pref itself, which the branches that made AccountSecrets::stored() the one read (a deleted account owns no key) did not know of; StoredSecretsReadThroughAccountSecretsTest holds every adapter to it. Also: #323's session runner refuses a web request first (#306).
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: DH99 ("The JVZoo IPN URL points to a file that has never existed")
What changed
tracking202/static/jvzoo.phpnever existed. Account › API integrations,GET /system/integrationsandp202 system integrationsall handed it out, so every JVZIPN sent there got a 404. The file now runs #268'sReceiverwith a JVZoo adapter (202-config/PaymentNotification/JVZoo/).Source. I read JVZoo's own article, "JVZIPN - How to create your own integrated script" (support.jvzoo.com article 206456857, updated 2024-06-13). The support site sits behind a Cloudflare challenge, but its Zendesk Help Center API is open. I also read the JVZIPN v2 guide, "Custom Variables", "How to set up tracking IDs", "Test Purchase Mode", and the refund articles. Where they differ from the research brief, JVZoo's documents are followed (see the decisions below).
Two URLs, one per role. Every JVZIPN carries two click fields:
caffitidis the tid on an affiliate's link.cvendthruholds the variables on the vendor's buy link.Each field is set by the other party, so neither can be trusted without knowing which party received the notification. Any affiliate can set the tid that lands in a vendor's notification. JVZoo itself asks for the URL in two places, so each place gets its own URL:
jvzoo.php?role=affiliategoes in the affiliate's Affiliate IPN (Affiliates › Approved Products › Integrations). It reads onlycaffitid, which the campaign's offer URL fills with?tid=[[subid]]on the JVZoo affiliate link.jvzoo.php?role=vendorgoes in the product's JVZIPN v1 URL (Advanced Settings). It reads onlysubidincvendthru, which the buy link fills with?subid=[[subid]].A URL with no role is answered 400.
Verification. The
cverifyis computed by JVZoo's published validator:cverify, in field-name order, each followed by|.It is compared with
hash_equals(). JVZoo's sample uses==, which would accept+12345678when the cverify is all digits. It is checked against every account's stored key before anything is read, and the account is the one whose key verifies it (CLAUDE.md #16).Dating. Every money event is dated by
ctranstime, which goes toNotification::$occurredAt. #268's Processor holds that time to the click for a sale, to the sale for a refund, and to the notification's arrival, and the answer says when it did.ctranstimemust be a Unix time of 1 to 10 digits, at mostNotification::LAST_TIME. Anything else is refused with400, namingctranstime.What each
ctransactiondoes:ctransactionSALE,BILLjvzoo, new) underctransreceipt, atctransamountRFND,CGBK,INSFctransreceiptnamesCANCEL-REBILL,UNCANCEL-REBILL,TEST200, records nothing400, naming itDuplicates and changed amounts are handled by #268's Processor. A JVZoo refund has no id of its own; it comes under the sale's receipt. So #268 keys it by its body:
The key.
JVZooKeyis the one rule. It is used by the page, byPUT /users/{id}/preferences(moved fromTEXTto its own rule inPreferenceRules) and by the endpoint:secret_storedis true only for a key the endpoint can sign with.The panel gives the steps for an affiliate and for a vendor, both URLs, and where JVZoo shows the key (My Account › Account Details › Seller Settings).
GET /system/integrationsnow lists:jvzoo: label "Affiliate IPN URL",…/jvzoo.php?role=affiliate;jvzoo_vendor(new): label "Vendor JVZIPN URL",…/jvzoo.php?role=vendor.Docs.
openapi.yaml(integrations, the preference, the source enum),cli.md,cli-agent.md,documentation/api/{09,10,14,15},documentation/cli/10-go-cli.md, a JVZoo section with an outcome table in09-3rd-party-integrations.md, and both changelogs. The Go CLI's--sourcelist now includesjvzoo.Changes to #268's code
Processor::sale()409 for a sale whose reversal still stands first went in here. 1.9.79: PayKickstart's product IPN, verified with each campaign's key #268 has since ported it (b000000), so the merge carries 1.9.79: PayKickstart's product IPN, verified with each campaign's key #268's own copy. JVZoo documents that undoing a refund sendsRFND, thenSALEagain on the same receipt. This PR's only edits to 1.9.79: PayKickstart's product IPN, verified with each campaign's key #268's files are the structural lists below.jvzoo.phpbesidepaykickstart.php(PostReadersCheckTokenTest,WebEventPathsTest,StoredVisitorIpSourceTest), andKNOWN_DEADno longer hasjvzoo.php.Decisions a reviewer should check
https://URL.ctransvendor/ctransaffiliate. That would need a new column and a page field, and it matches the two settings JVZoo already has less closely.200. Also, a click lookup before verification would tell anyone without a key which click ids exist.INSFreverses the sale; it is not "record nothing" as the brief said. JVZoo definesINSFas "an eCheck chargeback for a standard or recurring product".cupsellreceiptis an upsell's parent receipt, not a refund's original.ctransamountmust have a decimal point. JVZoo's field table, and its v2 migration table, say pennies (1000= $10.00). Its sample POST and real notifications (a production log on GitHub) say dollars (10.00,0.10).47.00is read as dollars.4700is refused with400, naming the conflict, rather than guessed.TEST, soTESTis acknowledged. Every other undocumented type is refused.ctransamountreadsctranstimeas the receipt, which would record the sale a second time. The testtestValuesMovedBetweenFieldsUnderOneCverifyAreNotReadshows it.400naming that field.paykey|customer_email|product_name|transaction_type|date) and covers neither the amount nor the tid.400. It is not signed: what JVZoo's validator does with it differs between PHP versions (measured on 8.3: each invalid byte becomes?).200 no_click, because a tid is general-purpose.subidthat is not a click id is400, because the panel says to putsubid=[[subid]]there; this matches PayKickstart.no_clickis answered by the adapter as aRefusal, so its wording names the right field. Its log line has the receipt in the message but not the structured fields.cupsellreceiptis not used to find the parent's click.ctranstimeon a refund were the original sale's time, rather than the refund's. Then a second genuine refund after a reinstatement would be byte-identical to the first, and it would be answeredduplicate. No receiver can tell those two apart. JVZoo's article does not say whose time a refund carries.GET /users/{id}/preferencesandGET /system/integrationscan disagree. After 1.9.79: GET /users/{id}/preferences serves no integration secret #270, the preferences read serves the JVZoo key as{"stored": true}whenever the column holds text.secret_storedin the integrations read is true only for a key the endpoint can sign with. So a stored key with a line break in it reads asstored: truein one andsecret_stored: falsein the other.Verification
After the merges of #268 (10/28)
Scope report of
.claude/skills/p202-verify/scripts/verify.sh --since origin/1.9.79-paykickstart-ipn --changed, verbatim. It ran on the tree with #268 at 41bfe59 and thectranstimechange (PHP 8.3.6, MariaDB 10.11, fullvendor/).phpcs passes here because the ratchet's baseline is HEAD. The tab-indented panel lines explained under the first round were already in HEAD, so nothing was counted as added.
#268's 8532b44 merged cleanly after that run. It touches only
PayKickstartKeysand its panel. After it, I ran:tests/PaymentNotification(unit, 111 tests);HandedOutUrlsNameServedFilesTestandComponentClassIsConsumedTest;Integration suites, each on a database recreated for it:
JVZooIpnIntegrationTest: 15 tests. New: a late notification, where a sale 3 days late and its refund 2 days late keep their own times, a chargeback stating a time before its sale is dated at the sale, actranstimechanged after signing is a 403, and one that is not a Unix time is a 400. Also new: the refund-after-delete path (the first refund sent again is a duplicate, a later one reverses) and the 403'scverifywording.PayKickstartIpnIntegrationTest: 20 tests.AccountAdministrationIntegrationTest(13) andPreferencesWriteIntegrationTest(7).Plants, each caught by a named test:
ctranstimedropped: caught bytestALateNotificationIsDatedByItsCtranstime;ctranstimeread as none, which is the old lenient read: caught bytestAFieldTheNotificationNeedsIsReadExactlyOrRefusedByName;signatureField()answeringhash: caught bytestNothingIsWrittenForANotificationTheStoredKeyDoesNotVerify.Live pass on 10/28, without the licence. The licence stub is down. I installed with
install-instance.sh's own steps, skipping the licence form. The Go CLI answers 402 on that install, so the setup went through the v3 API with curl:POST /ppc-networks,/ppc-accounts,/campaigns(offer URLhttps://www.jvzoo.com/c/17/414851/?tid=[[subid]], accumulate) and/trackers;PUT /users/1/preferenceswithjvzoo_ipn_secret_key: " Live-JVZIPN-Key-0001 ", stored trimmed (20 characters).GET /system/integrationsgave both URLs withsecret_stored: true, and the tracker redirected to…?tid=1and…?tid=2. Notifications were signed by a script independent of this code and POSTed with curl:200 recorded;200 reversed;409, naming conversion 2;DELETE /api/v3/conversions/2204;200 duplicate;200 duplicate("was reversed by conversion 2, since deleted; this RFND is that refund sent again");200 reversed, conversion 3.conv_timeis thectranstimeit stated, not when it arrived (about 30 seconds later).200, "Dated at the click … is before the click";200, "Dated when it arrived … later than this server's clock";ctranstime=2026-10-28was400, namingctranstime.403, "The notification's cverify does not verify …".Could not run on 10/28 (licence stub down):
p202 conversion list --source jvzooand the agent-eval case;First round (10/23)
Scope report of
.claude/skills/p202-verify/scripts/verify.sh --since origin/1.9.79 --changed, run on the final tree before committing, verbatim. Environment: PHP 8.3.6, MariaDB 10.11, fullvendor/. The unit tier ran 6184 tests with 5 skipped: the 4 documentedUserTestDB-singleton skips, andCronEntryPointsFailLoudlyTest, because the live instance's202-config.phpwas in the checkout.Why phpcs FAILed in the first round. The phpcs ratchet counts
202-account/api-integrations.phpas made worse: 1010 errors, against 930 at #268's tip. All 80 added findings areGeneric.WhiteSpace.DisallowTabIndent, from the JVZoo panel's new lines. The file is indented with tabs throughout: 925 of those errors are already in it, and #268's panel just below adds the same kind of lines. Its other findings went down by one, and every new file is PSR-12 clean. I kept the file's indentation rather than mixing spaces into it.Run by hand, beyond the ladder:
JVZooIpnIntegrationTest(14). It covers: an affiliate's sale; a vendor's sale that ignores the tid; a replay; a changed amount (409); a rebill; refund, refund replay, and chargeback-after-refund (duplicate); CGBK and INSF reversals; a refund with nothing to reverse (404); a sale re-sent after its refund (409, and a duplicate once the reversal is deleted); an unknown key, a tampered amount, a moved tid, a missing cverify and GET (403/405); another account's click and a missing click (404, the same answer); a key shared by two accounts; CANCEL-REBILL, UNCANCEL-REBILL, TEST, no click, no role and a whole-number amount; an unreadable stored key; and the 503 while the upgrade is pending.PayKickstartIpnIntegrationTest(15), with the Processor change.AccountAdministrationIntegrationTest(13) andPreferencesWriteIntegrationTest(7).JVZooSignatureTest):AACDD10EandD1EA7E5Afor the keysecret. JVZoo's validator, run verbatim, reproduces both.-B002), non-ASCII text, whitespace (not trimmed), a|in a value, and a refund under another key.cvendthru;==, and a case-blind compare;secret_storedfor any text, and the API writing any text;jvzoo.phpdeleted (bothHandedOutUrlsNameServedFilesTesttests).install-instance.shand seeded withseed.sh:Live-JVZIPN-Key-0001was saved, stored trimmed, and the pill turned Connected. The panel showed both URLs.p202 system integrationslistedjvzooandjvzoo_vendorwithsecret_storedtrue. A second account's key went in throughp202 user prefs update 2 --jvzoo-ipn-secret-key, and a two-line key was refused there with422.https://www.jvzoo.com/c/17/414851/?tid=[[subid]], and tracker B redirected to…?tid=7. Then it was the buy linkhttps://www.jvzoo.com/b/0/414851/1?subid=[[subid]], and the tracker redirected to…?subid=8.200 recordedon click 7: the vendor subid in the same body was ignored. Then:200 duplicate; another amount409;200 recorded;200 reversed; refund replay200 duplicate; chargeback after it200 duplicate;409;404;200 reversed;403; tampered amount403; no cverify403;200 no_click; tidfbweek1200 no_click;404; the other account's key on click 7404;400;4700400; an extra field400;200;415; a v2 body415; GET405;subid=8200(the affiliate tid in the same body ignored), no subid200 no_click,subid=[[subid]]400, refund200 reversed.409andp202 conversion delete 9(the reversal), the sale again was200 duplicateand the click was at 56.p202 conversion list --source jvzooandp202 click conversions 7,8and9read the rows back. The server log held one line per notification (36 for 36), with no key, email or buyer name.node tests/browser/run.js --spec account-pageswith a new JVZoo scenario: two URLs to copy, in order; Not connected, then Connected; the disclosure closed; the key saved trimmed. 591 checks passed, including the API integrations baseline at 1280px and 390px, light and dark.admin-006-a-jvzoo-affiliate-gets-the-affiliate-url, with a reference-agent branch.subidfailed all six mechanical expectations.searchKnownMisses: two tasks in one ask, as with admin-005.Could not run
400, naming them.tests/run-integration-suites.sh) was not run; only the four suites this change touches were.tests/live/upgrade-equals-install.shwas not run: this change makes no schema change. Thejvzoosource fits the existingvarchar(32), and the key column exists.Found outside this item
signatureField());GET /users/{id}/preferencesservedjvzoo_ipn_secret_keyin plain text (1.9.79: GET /users/{id}/preferences serves no integration secret #270, merged into 1.9.79: PayKickstart's product IPN, verified with each campaign's key #268).{tid},{affiliate_amount},{transaction_id}and{transaction_type}). These could point at Prosper202's own postback URL. The panel does not mention them.🤖 Generated with Claude Code
https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr