feat: add sendPaddlePurchase#96
Merged
Merged
Conversation
Surfaces a Paddle-aware counterpart to sendStripePurchase. Same shape,
same endpoint (POST /v3/users/{uid}/purchases), same instant-grant
flow on the backend — the api-gateway DEV-937 change accepts a new
`paddle_store_data` field that purchaseman already knows how to
validate (live → sandbox dual-probe, GetSubscription / GetTransaction)
and feed into productCenter.UpdateClientProduct synchronously.
Public API:
Qonversion.shared.sendPaddlePurchase({
price, currency, purchased,
transactionId, customerId, productId, type, subscriptionId?
}) → Promise<UserPurchase>
Wire flow:
QonversionInstance.sendPaddlePurchase
→ QonversionInternal.sendPaddlePurchase
→ PurchasesController.sendPaddlePurchase (logger + userId lookup)
→ PurchasesService.sendPaddlePurchase
→ RequestConfigurator.configurePaddlePurchaseRequest
builds POST /v3/users/{userId}/purchases body
{ price, currency, purchased,
paddle_store_data: { transaction_id, customer_id,
product_id, type,
subscription_id? (omitted for inapp) } }
→ api-gateway → purchaseman → Paddle API → product-center → done.
UserPurchase.stripeStoreData / paddleStoreData are now both optional
since a purchase row carries exactly one. PurchaseService grew a
small private executePurchaseRequest helper to dedupe the
success-or-throw shape between Stripe and Paddle.
Tests:
- PurchasesService: paddle success + paddle error mirroring stripe.
- PurchasesController: paddle success + paddle error.
- QonversionInternal: sendPaddlePurchase delegation + log line.
- RequestConfigurator: paddle subscription request + paddle inapp
request (asserting subscription_id is omitted from the inapp body).
`yarn test` → 220/220. `yarn tsc --noEmit` clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses the code-review findings on sendPaddlePurchase: * Wire-enum mismatch: the SDK was sending `type: "inapp"` for one-time purchases, but the api-gateway's shared product-type enum is "non_recurring". Mapping now lives in RequestConfigurator (write: inapp → non_recurring) and PurchaseService (read: non_recurring → inapp). SDK callers keep the Paddle-native "inapp" string; the wire conversation matches the server contract. * Restore back-compat for UserPurchase consumers. Splits the type into UserStripePurchase + UserPaddlePurchase (each with its store data required), and exposes UserPurchase as their union. The send*Purchase methods return the narrow variant, so existing Stripe-only consumers that did `purchase.stripeStoreData.productId` still compile without changes. * executePurchaseRequest helper now typed as NetworkRequest instead of the awkward ReturnType<...> lookup, and is generic so each send*Purchase method returns its narrow type. * Drop the gendered phrasing in the new JSDoc; align Stripe JSDoc too for consistency. * New test asserts the inapp wire round-trip: api returns type:"non_recurring", SDK surfaces type:"inapp". * Updated RequestConfigurator inapp test to assert the wire enum value, not the SDK enum value. The matching api-gateway-side contract test (raw JSON POST asserting the gateway accepts "non_recurring") lands in api-gateway as part of the same review response. yarn test → 221/221, yarn tsc --noEmit clean.
Addresses review SHOULD-FIX #5 and NIT #8: * Replace the ternary `data.type === 'inapp' ? 'non_recurring' : 'subscription'` with a switch + `never` exhaustiveness guard. Adding a future PaddlePurchaseType variant now fails the type checker here instead of silently aliasing to "subscription" at runtime. * On the read path, drop the `(purchase.paddleStoreData.type as string)` cast + in-place mutation pattern. Convert the wire literal to the SDK literal through a dedicated `paddleWireTypeToSdk` helper with the same exhaustive switch shape. The boundary is now an explicit PaddleStoreDataApi → PaddleStoreData transformation instead of a cast. yarn test → 221/221, yarn tsc --noEmit clean.
The gateway and purchaseman never persist or act on paddle_store_data.customer_id — it was accepted by every layer but dropped after validation. Removing the field aligns the SDK with the companion purchaseman (#491) and api-gateway (#556) PRs that drop the matching openapi field. Changes: - PaddleStoreData: drop customerId - PaddleStoreDataApi (wire): drop customer_id - RequestConfigurator: drop customer_id from request body - Tests: drop customerId fixtures everywhere Caller impact: nothing — customer_id was never required to flow through; it's already in the SDK consumer's context. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
Adds
sendPaddlePurchaseas the Paddle counterpart tosendStripePurchase. After a checkout completes in Paddle.js, call this method with thetransaction_id(andsubscription_idfor subscriptions) and the backend validates the purchase and grants entitlements before the promise resolves.Wire flow
QonversionInstance.sendPaddlePurchaseQonversionInternal.sendPaddlePurchasePurchasesController.sendPaddlePurchase(logger + user id lookup, mirrors the stripe path)PurchasesService.sendPaddlePurchaseRequestConfigurator.configurePaddlePurchaseRequest—POST /v3/users/{userId}/purchaseswith body:{ "price": "...", "currency": "...", "purchased": ..., "paddle_store_data": { "transaction_id": "txn_...", "customer_id": "ctm_...", "product_id": "pro_...", "type": "subscription", "subscription_id": "sub_..." } }type: 'inapp') purchasessubscription_idis omitted from the body entirely.Notes
UserPurchase.stripeStoreDataandUserPurchase.paddleStoreDataare now both optional — a purchase row carries exactly one.PurchaseServicegrew a small privateexecutePurchaseRequesthelper to dedupe the success-or-throw shape between Stripe and Paddle.Test plan
yarn test→ 220/220 (Jest)yarn tsc --noEmitcleanPurchasesService.test.ts: paddle success + paddle errorPurchasesController.test.ts: paddle success + paddle errorQonversionInternal.test.ts:sendPaddlePurchasedelegation + log lineRequestConfigurator.test.ts: paddle subscription request body + paddle inapp request body (assertssubscription_idis omitted)