pay-whop 0.1.0.alpha.4 → 0.1.0.alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +101 -118
- data/app/controllers/pay/whop/webhooks_controller.rb +11 -1
- data/app/models/pay/whop/customer.rb +36 -5
- data/app/models/pay/whop/subscription.rb +4 -2
- data/db/migrate/20261113000000_allow_shared_whop_members.rb +24 -0
- data/docs/advanced-usage.md +2 -0
- data/docs/demo.md +36 -0
- data/docs/qa-2026-11-12.md +28 -0
- data/docs/releasing.md +6 -6
- data/docs/shared-buyers.md +17 -0
- data/docs/verification.md +9 -3
- data/docs/worklog/2026-11-12-demo-qa.md +7 -0
- data/docs/worklog/2026-11-12-repository-presentation.md +8 -0
- data/lib/pay/whop/version.rb +1 -1
- data/lib/pay/whop.rb +1 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d181dedf94c5f676f9678fa0eb452888cd4b9b17bc5491a77fa0edc05538f048
|
|
4
|
+
data.tar.gz: 4d63466ec62eabc795197d1e2a7cdc4476b3b0a3daf15f6d10ca4b621da64fe0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 95806d6bfb6bd3e27d5f49e91e985ba4f6c7da3f0e1518ee75ceadb4305c54c3ff1610b8c862ee29ed7f2f85ac9ffc5f32d2b9d3a38b8c46250f34beec3a9e74
|
|
7
|
+
data.tar.gz: f2224d7bed6966f059057b44619e7e529fe52e23e35188970614688c88318799f27958957bee27fa7a669058051c6fafee0fe59a19a2b485ed954cf4921383b1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0.alpha.5
|
|
4
|
+
|
|
5
|
+
- Preserve explicit adaptive-pricing settings on inline checkouts instead of losing them in SDK serialization.
|
|
6
|
+
- Expire trial access at its confirmed end even when the renewal webhook is delayed. Add customer-scoped membership reads/synchronization for verified free-trial returns, and reject another customer's signed resource before binding its member.
|
|
7
|
+
- Allow hosts to opt into multiple Pay customers for one Whop member with `Pay::Whop.allow_shared_members = true`. Each payment or membership must carry a valid signed customer reference when the member is ambiguous; an already linked customer cannot change its member or seller.
|
|
8
|
+
- Replace Pay's global processor/member uniqueness constraint with a non-Whop unique index and a Whop lookup index. Install the new migration before enabling shared buyers. Rolling it back requires first resolving duplicate Whop member mappings; it does not merge or delete records.
|
|
9
|
+
- Reconcile signed refund webhooks containing either `payment_id` or nested `payment.id`, as observed in a genuine sandbox refund. Reject conflicting references and re-read the canonical payment before applying the refund.
|
|
10
|
+
|
|
3
11
|
## 0.1.0.alpha.4
|
|
4
12
|
|
|
5
13
|
- Keep reversed payouts terminal when overlapping reads return an older completed state, for both platform and seller payouts.
|
data/README.md
CHANGED
|
@@ -1,68 +1,71 @@
|
|
|
1
1
|
# pay-whop
|
|
2
2
|
|
|
3
|
-
Whop payments
|
|
3
|
+
**Whop payments for Ruby on Rails, through [Pay](https://github.com/pay-rails/pay).**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://github.com/yshmarov/pay-whop/actions/workflows/ci.yml)
|
|
6
|
+
[](https://rubygems.org/gems/pay-whop)
|
|
7
|
+
[](pay-whop.gemspec)
|
|
8
|
+
[](LICENSE)
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
Hosted checkout, subscriptions, saved payment methods, refunds and marketplace subaccounts using familiar Pay models and Whop's official Ruby SDK.
|
|
8
11
|
|
|
9
|
-
**
|
|
12
|
+
[**Try the live demo**](https://pay-whop.onrender.com/) · [Install](#installation) · [Usage guide](docs/advanced-usage.md) · [Compatibility](docs/compatibility.md) · [Changelog](CHANGELOG.md)
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
user.set_payment_processor(:whop)
|
|
14
|
+
[](https://pay-whop.onrender.com/)
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
variant_id: "plan_...",
|
|
16
|
-
redirect_url: billing_url,
|
|
17
|
-
idempotency_key: checkout_attempt.id.to_s
|
|
18
|
-
)
|
|
19
|
-
redirect_to checkout.purchase_url, allow_other_host: true
|
|
16
|
+
**Community alpha.** Published on RubyGems as `0.1.0.alpha.5`. Independently maintained; not an official Whop or Pay integration. Start in the sandbox and review the [verification evidence](docs/verification.md) before production use.
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
user.payment_processor.subscribed?
|
|
23
|
-
user.payment_processor.subscription.cancel
|
|
24
|
-
user.payment_processor.charges.last.refund!
|
|
25
|
-
```
|
|
18
|
+
## What you can build
|
|
26
19
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
| Capability | Status |
|
|
20
|
+
| Flow | Adapter support |
|
|
30
21
|
| --- | --- |
|
|
31
|
-
| Hosted checkout for
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
22
|
+
| Sell once or bill repeatedly | Hosted checkout for existing or inline Whop variants; one-time charges and recurring subscriptions |
|
|
23
|
+
| Manage billing | Cancel, resume, pause, free days, promo codes and permitted renewal price/date/cadence changes |
|
|
24
|
+
| Charge saved methods | Setup checkout, direct charges, authorization/capture/void, failed-payment retry and 3DS recovery |
|
|
25
|
+
| Refund payments | Full and partial refunds using exact currency precision and confirmed provider state |
|
|
26
|
+
| Run a marketplace | Seller subaccounts, hosted onboarding/payout links, separate seller-scoped buyers and application fees |
|
|
27
|
+
| Keep Rails in sync | Signed webhooks, replay protection, application callbacks, recovery jobs and reconciliation |
|
|
28
|
+
| Extend billing workflows | Invoice and dispute summaries, one-time entitlements, balances, transfer and payout status |
|
|
29
|
+
|
|
30
|
+
See the [full compatibility matrix](docs/compatibility.md) for Pay method coverage and provider restrictions. Plan swaps, Stripe-style proration, recurring seat changes and metered billing are unsupported. Invoice issuance and internal transfers are implemented but remain blocked by verification in this sandbox; bank payouts are unavailable in Whop's sandbox. [Details and sources →](docs/advanced-usage.md)
|
|
31
|
+
|
|
32
|
+
## Tested, with evidence
|
|
33
|
+
|
|
34
|
+
- **12 CI combinations:** Ruby 3.3 / 3.4 / 4.0 × Pay 12.3 / 12.4 × SQLite / PostgreSQL, using Rails 8.1.
|
|
35
|
+
- **109 tests, 423 assertions** in the `0.1.0.alpha.5` release suite, plus Standard Ruby linting. Tests exercise real Pay models, signed webhook requests and the SDK/HTTP boundary without payment credentials.
|
|
36
|
+
- **Fresh-app installation:** the published gem was downloaded, its SHA-256 checked against the release artifact, and installed/migrated/eager-loaded in a separate Rails application.
|
|
37
|
+
- **Actual sandbox runs:** recurring and one-time checkout, marketplace fees, cancellation, full/partial refunds, saved methods, authorization/capture/void and signed child webhooks. [Observed results and remaining gaps →](docs/verification.md)
|
|
38
|
+
|
|
39
|
+
HTTP fixtures, sandbox observations and live-money validation are different levels of evidence. The [verification log](docs/verification.md) states which applies to each flow; production processing and complete renewal/SCA/bank-settlement lifecycles remain unverified.
|
|
40
|
+
|
|
41
|
+
<details>
|
|
42
|
+
<summary>See real sandbox billing and refund records</summary>
|
|
43
|
+
|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
Captured from the deployed demo on 12 November 2026. Synthetic customer records and test money; checkout totals can include tax.
|
|
47
|
+
|
|
48
|
+
</details>
|
|
49
|
+
|
|
50
|
+
<details>
|
|
51
|
+
<summary>Explore the separately labelled invoice and transfer simulations</summary>
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
[Try these simulations](https://pay-whop.onrender.com/#simulations) without a Whop account or KYC. They update separate local demo records, with `sim_` references and a resettable history. They never call Whop, create Pay charges, grant access or move funds. A simulated outcome does not verify real invoice issuance or transfer settlement.
|
|
56
|
+
|
|
57
|
+
</details>
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
Requires Ruby 3.3+, Pay 12.3–12.x and Whop Ruby SDK 2.7.x. The demo and installation smoke test use Rails 8.1. Requests are pinned to Whop API date `2026-10-28-1`.
|
|
59
62
|
|
|
60
63
|
```ruby
|
|
61
64
|
# Gemfile
|
|
62
|
-
gem "pay-whop",
|
|
65
|
+
gem "pay-whop", "0.1.0.alpha.5"
|
|
63
66
|
```
|
|
64
67
|
|
|
65
|
-
Install both engines' migrations, including Pay's STI and object
|
|
68
|
+
Install both engines' missing migrations, including Pay's STI and object columns:
|
|
66
69
|
|
|
67
70
|
```sh
|
|
68
71
|
bundle install
|
|
@@ -71,9 +74,10 @@ bin/rails pay_whop:install:migrations
|
|
|
71
74
|
bin/rails db:migrate
|
|
72
75
|
```
|
|
73
76
|
|
|
74
|
-
Configure the adapter in an initializer. Keep credentials
|
|
77
|
+
Configure the adapter in an initializer. Keep credentials in environment variables or your secret manager:
|
|
75
78
|
|
|
76
79
|
```ruby
|
|
80
|
+
# config/initializers/pay_whop.rb
|
|
77
81
|
Pay::Whop.configure do |config|
|
|
78
82
|
config.environment = ENV.fetch("WHOP_ENVIRONMENT", "sandbox")
|
|
79
83
|
config.api_key = ENV.fetch("WHOP_API_KEY")
|
|
@@ -82,7 +86,7 @@ Pay::Whop.configure do |config|
|
|
|
82
86
|
end
|
|
83
87
|
```
|
|
84
88
|
|
|
85
|
-
|
|
89
|
+
Add Pay to your billable model:
|
|
86
90
|
|
|
87
91
|
```ruby
|
|
88
92
|
class User < ApplicationRecord
|
|
@@ -90,89 +94,68 @@ class User < ApplicationRecord
|
|
|
90
94
|
end
|
|
91
95
|
```
|
|
92
96
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
Use an Account API key with permissions for checkout configuration creation, member reads, membership reads/updates/cancellation, and payment reads/refunds. Product/variant and webhook-management permissions are needed only if your application provisions those resources. Whop's sandbox Admin role can create checkouts; its `accounts.me` endpoint additionally requires balance-read access, so the connection checker deliberately uses the checkout API.
|
|
96
|
-
|
|
97
|
-
Run an Active Job worker. Production needs a durable queue adapter. The example uses PostgreSQL-backed GoodJob in production and the in-memory `:async` adapter for local development.
|
|
98
|
-
|
|
99
|
-
## How synchronization works
|
|
100
|
-
|
|
101
|
-
1. Checkout metadata contains a signed Pay customer ID, scoped to the seller account and environment. Treat the customer-specific checkout URL as private: Whop checkout configurations are reusable links, not single-use sessions.
|
|
102
|
-
2. The webhook endpoint verifies Whop's signature and timestamp, checks the seller account, then persists the event ID, type and resource ID. It does not persist the raw webhook body.
|
|
103
|
-
3. A job retrieves the current payment or membership from Whop. It verifies the account again and resolves the signed customer reference or an already-linked Whop member.
|
|
104
|
-
4. Pay records are updated under a customer row lock. Provider `updated_at` values prevent older snapshots from overwriting newer ones. API calls run outside database transactions.
|
|
105
|
-
5. Payment records become Pay charges only when their provider status is `paid`. A checkout redirect, pending payment or authorized card is not a paid charge.
|
|
97
|
+
Register `POST /pay/whop/webhooks` in Whop with the event list in `Pay::Whop::Delivery::EVENTS`, the same API date, and the signing secret verbatim (including its `ws_` prefix). Enable subaccount events for marketplaces. Run a durable Active Job worker; the demo uses PostgreSQL-backed GoodJob in production.
|
|
106
98
|
|
|
107
|
-
|
|
99
|
+
Use an Account API key with the permissions for the features you enable. Checkout needs checkout-configuration creation and related product/variant access; synchronization needs member, membership and payment reads. [Feature-specific permissions →](docs/advanced-usage.md#permissions-and-operations)
|
|
108
100
|
|
|
109
|
-
|
|
101
|
+
## Your first checkout
|
|
110
102
|
|
|
111
|
-
|
|
103
|
+
Use an existing Whop recurring variant and an HTTPS return URL:
|
|
112
104
|
|
|
113
105
|
```ruby
|
|
114
|
-
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
Schedule that recovery job periodically (it defaults to deliveries older than two minutes, with no one-day cutoff): a process can stop after the database write but before job enqueue. Monitor failed jobs rather than treating an HTTP 200 webhook acknowledgment as completed synchronization. Completed delivery IDs are retained for deduplication; choose a retention policy before running at scale.
|
|
118
|
-
|
|
119
|
-
## Billing differences
|
|
120
|
-
|
|
121
|
-
- Whop variants map to Pay's `processor_plan`, memberships to subscriptions, members to customers, and payments to charges.
|
|
122
|
-
- A scheduled cancellation retains access through the current billing period. `resume` reverses that scheduled cancellation; it does not reactivate an ended subscription.
|
|
123
|
-
- `refund!` asks Whop to refund the remaining balance, or the specified integer minor-unit amount, then retrieves current payment and membership state. Refunds settle asynchronously and do not themselves cancel a subscription; cancel separately when access should end. Insufficient seller funds raise `Pay::Whop::ApiError` and leave the local refund amount unchanged.
|
|
124
|
-
- Pay stores integer minor units. Whop money objects are converted with `BigDecimal` and the provider's currency precision, without rounding fractions away.
|
|
125
|
-
- Amounts currently use Whop's account-facing `total` and `refunded_amount`, not buyer fees. Currency mismatches are rejected; adaptive-pricing/FX refunds and tax reconciliation require further validation before enabling them.
|
|
126
|
-
- Supply a stable `idempotency_key:` for each checkout, charge, cancellation, resumption, invoice, transfer or refund attempt. Reuse it when retrying the same operation after a timeout. A new operation needs a new key. SDK automatic retries are disabled so unexamined writes are not automatically repeated.
|
|
127
|
-
|
|
128
|
-
## Run the example
|
|
106
|
+
customer = current_user.set_payment_processor(:whop)
|
|
129
107
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
bundle exec examples/rails/bin/setup
|
|
137
|
-
cd examples/rails
|
|
138
|
-
bundle exec bin/rails server -b 127.0.0.1 -p 3001
|
|
108
|
+
checkout = customer.checkout(
|
|
109
|
+
variant_id: "plan_...",
|
|
110
|
+
redirect_url: billing_url,
|
|
111
|
+
idempotency_key: checkout_attempt.id.to_s
|
|
112
|
+
)
|
|
113
|
+
redirect_to checkout.purchase_url, allow_other_host: true
|
|
139
114
|
```
|
|
140
115
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
Whop requires an HTTPS checkout return URL. Set `WHOP_RETURN_URL` to an HTTPS page you control; for local testing it can be a static page that links back to localhost. Expose only the webhook route through your local tunnel, register that HTTPS URL in Whop, and set `WHOP_WEBHOOK_SECRET` before restarting the server. Checkout returns to the billing page; refresh after the webhook arrives. A successful subscription, cancellation and refund should be visible in both Pay and Whop.
|
|
116
|
+
After the verified webhook has synchronized the provider state:
|
|
144
117
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
118
|
+
```ruby
|
|
119
|
+
current_user.payment_processor.subscribed?
|
|
120
|
+
current_user.payment_processor.subscription.cancel
|
|
121
|
+
current_user.payment_processor.charges.last.refund!(
|
|
122
|
+
idempotency_key: refund_request.id.to_s
|
|
123
|
+
)
|
|
149
124
|
```
|
|
150
125
|
|
|
151
|
-
|
|
126
|
+
For a one-time purchase, use `customer.checkout_charge(amount: 500, redirect_url: billing_url, idempotency_key: order.id.to_s)`. Amounts accepted by convenience methods are integer minor units: `500` means USD 5.00. A successful one-time payment creates a charge, not a recurring subscription.
|
|
152
127
|
|
|
153
|
-
|
|
128
|
+
For marketplaces, create and verify a seller subaccount, then use `merchant.customer_for(user)` for that seller's checkout. Keep one platform account and one environment per Rails database. [Marketplace example →](docs/advanced-usage.md#marketplace--connect-style-flows)
|
|
154
129
|
|
|
155
|
-
|
|
130
|
+
## How synchronization works
|
|
156
131
|
|
|
157
|
-
|
|
132
|
+
1. Checkout metadata carries a signed Pay customer reference, scoped to the Whop account and environment. Customers are never matched by email.
|
|
133
|
+
2. The endpoint verifies the webhook signature and timestamp, checks the account, and persists the event/type/resource IDs without storing the raw webhook body.
|
|
134
|
+
3. A job retrieves the current Whop resource, checks its ownership, and updates local records under a row lock. Timestamped snapshots cannot overwrite newer state; terminal payout reversals cannot regress.
|
|
135
|
+
4. Only a provider payment with status `paid` creates a Pay charge. Returning from checkout or receiving HTTP 200 does not grant access.
|
|
158
136
|
|
|
159
|
-
|
|
137
|
+
API requests run outside database transactions. Supply a stable `idempotency_key:` for each write and reuse it when retrying the same operation. Pay stores exact integer minor units; provider status determines confirmed refunds and settlement. Whop messaging remains in effect; this adapter does not generate Pay receipt/subscription emails.
|
|
160
138
|
|
|
161
|
-
|
|
162
|
-
bin/test
|
|
163
|
-
# Or:
|
|
164
|
-
bundle exec rake test
|
|
165
|
-
bundle exec standardrb
|
|
166
|
-
```
|
|
139
|
+
Callbacks are at least once. Failed deliveries remain available for recovery, and applications can schedule:
|
|
167
140
|
|
|
168
|
-
|
|
141
|
+
```ruby
|
|
142
|
+
Pay::Whop::RecoverDeliveriesJob.perform_later
|
|
143
|
+
```
|
|
169
144
|
|
|
170
|
-
|
|
145
|
+
Monitor pending deliveries and `delivery_failed.pay_whop`. A successful webhook acknowledgment means the event was retained, not that synchronization has completed. Customer-specific checkout links are reusable and should be treated as private. [Errors, callbacks and reconciliation →](docs/advanced-usage.md#errors-callbacks-and-recovery)
|
|
171
146
|
|
|
172
|
-
|
|
173
|
-
- [Whop sandbox quickstart](https://docs.whop.com/developer/quickstart)
|
|
174
|
-
- [Whop Ruby SDK](https://github.com/whopio/whopsdk-ruby)
|
|
175
|
-
- [Whop webhook verification](https://docs.whop.com/developer/guides/webhooks)
|
|
176
|
-
- [Whop API stability](https://docs.whop.com/api-reference/stability)
|
|
147
|
+
## Try it, extend it, report an issue
|
|
177
148
|
|
|
178
|
-
|
|
149
|
+
| Resource | What you will find |
|
|
150
|
+
| --- | --- |
|
|
151
|
+
| [Live sandbox playground](https://pay-whop.onrender.com/) | Test checkout, saved methods, refunds and session-scoped billing. Free hosting may take about a minute to wake up. |
|
|
152
|
+
| [Run or deploy the demo](docs/demo.md) | Local setup, HTTPS returns, webhook configuration and Render/Neon hosting |
|
|
153
|
+
| [Advanced usage](docs/advanced-usage.md) | Billing, marketplaces, invoices, disputes, retries, permissions and recovery |
|
|
154
|
+
| [Compatibility matrix](docs/compatibility.md) | Supported Pay operations and differences from Stripe |
|
|
155
|
+
| [Verification evidence](docs/verification.md) | Tested versions, actual sandbox results and open validation gaps |
|
|
156
|
+
| [Contributing](CONTRIBUTING.md) | Development setup, test commands and useful bug reports |
|
|
157
|
+
| [Report a bug](https://github.com/yshmarov/pay-whop/issues/new/choose) | Reproduction and environment details, without credentials or buyer data |
|
|
158
|
+
| [Security reporting](SECURITY.md) | Private vulnerability reports |
|
|
159
|
+
| [Releases](https://github.com/yshmarov/pay-whop/releases) | Changelog and downloadable gem artifacts |
|
|
160
|
+
|
|
161
|
+
Maintained by [Yaroslav Shmarov](https://github.com/yshmarov). Built on [Pay](https://github.com/pay-rails/pay) and [Whop's official Ruby SDK](https://github.com/whopio/whopsdk-ruby). Available under the [MIT license](LICENSE).
|
|
@@ -10,7 +10,7 @@ module Pay
|
|
|
10
10
|
return head :ok unless Delivery::EVENTS.include?(event["type"])
|
|
11
11
|
raise InvalidPayload, "Webhook ID mismatch" unless event.fetch("id") == request.headers["webhook-id"]
|
|
12
12
|
object = event.fetch("data")
|
|
13
|
-
resource_id = event["type"].start_with?("refund.") ? object
|
|
13
|
+
resource_id = event["type"].start_with?("refund.") ? refund_payment_id(object) : object.fetch("id")
|
|
14
14
|
raise InvalidPayload, "Missing resource ID" if resource_id.blank?
|
|
15
15
|
delivery = Delivery.create_or_find_by!(environment: Whop.environment, account_id: account, event_id: event.fetch("id")) do |record|
|
|
16
16
|
record.event_type = event.fetch("type")
|
|
@@ -26,6 +26,16 @@ module Pay
|
|
|
26
26
|
rescue ::StandardWebhooks::WebhookVerificationError, JSON::ParserError, KeyError, IdentityError, InvalidPayload
|
|
27
27
|
head :bad_request
|
|
28
28
|
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def refund_payment_id(object)
|
|
33
|
+
payment = object["payment"]
|
|
34
|
+
raise InvalidPayload, "Invalid refund payment" unless payment.nil? || payment.is_a?(Hash)
|
|
35
|
+
ids = [object["payment_id"], payment&.dig("id")].compact.uniq
|
|
36
|
+
raise InvalidPayload, "Missing or conflicting refund payment IDs" unless ids.one? && ids.first.is_a?(String) && ids.first.present?
|
|
37
|
+
ids.first
|
|
38
|
+
end
|
|
29
39
|
end
|
|
30
40
|
end
|
|
31
41
|
end
|
|
@@ -16,14 +16,22 @@ module Pay
|
|
|
16
16
|
bind_account!
|
|
17
17
|
raise ArgumentError, "Choose a variant or variant_id" if mode == "payment" && [variant_id, variant].compact.size != 1
|
|
18
18
|
raise ArgumentError, "Unknown checkout option" unless (options.keys - %i[affiliate_code currency payment_method_configuration three_ds_level]).empty?
|
|
19
|
+
if variant&.key?(:adaptive_pricing_enabled)
|
|
20
|
+
body = options.merge(account_id: account_id, variant: variant, mode: mode, redirect_url: redirect_url,
|
|
21
|
+
metadata: checkout_metadata(metadata, name: name)).compact
|
|
22
|
+
# SDK 2.7 omits the adaptive pricing setting from inline variants.
|
|
23
|
+
result = Whop.api_request(:post, "checkout_configurations", body: body, idempotency_key: idempotency_key)
|
|
24
|
+
return ::Whop_sdk::CheckoutConfigurations::Types::CreateCheckoutConfigurationsResponse.load(JSON.generate(result))
|
|
25
|
+
end
|
|
19
26
|
Whop.api.checkout_configurations.create(**options,
|
|
20
27
|
account_id: account_id, variant_id: variant_id, variant: variant, mode: mode, redirect_url: redirect_url,
|
|
21
28
|
metadata: checkout_metadata(metadata, name: name),
|
|
22
29
|
request_options: Whop.request_options(idempotency_key: idempotency_key))
|
|
23
30
|
end
|
|
24
31
|
|
|
25
|
-
def checkout_charge(amount:, redirect_url:, currency: "usd", decimals: 2, application_fee_amount: nil, **options)
|
|
32
|
+
def checkout_charge(amount:, redirect_url:, currency: "usd", decimals: 2, application_fee_amount: nil, adaptive_pricing_enabled: nil, **options)
|
|
26
33
|
variant = {currency: currency, billing_type: "one_time", initial_price: Whop.major_units(amount, decimals: decimals)}
|
|
34
|
+
variant[:adaptive_pricing_enabled] = adaptive_pricing_enabled unless adaptive_pricing_enabled.nil?
|
|
27
35
|
if application_fee_amount
|
|
28
36
|
raise ArgumentError, "Application fee must be below the price" unless application_fee_amount < amount
|
|
29
37
|
variant[:application_fee_amount] = Whop.major_units(application_fee_amount, decimals: decimals)
|
|
@@ -60,6 +68,10 @@ module Pay
|
|
|
60
68
|
def verify_resource!(object)
|
|
61
69
|
verify_configuration!
|
|
62
70
|
Whop.verify_account!(object, expected: account_id)
|
|
71
|
+
token = object.dig("metadata", "pay_customer")
|
|
72
|
+
if token.present? && self.class.find_signed(token, purpose: Whop.identity_purpose(account_id)) != self
|
|
73
|
+
raise IdentityError, "Whop resource belongs to another customer"
|
|
74
|
+
end
|
|
63
75
|
unless self.class.resolve!(object).id == id
|
|
64
76
|
raise IdentityError, "Whop resource belongs to another customer"
|
|
65
77
|
end
|
|
@@ -137,6 +149,19 @@ module Pay
|
|
|
137
149
|
result
|
|
138
150
|
end
|
|
139
151
|
|
|
152
|
+
def membership(id)
|
|
153
|
+
result = Whop.api.memberships.retrieve(id: id, request_options: Whop.request_options)
|
|
154
|
+
object = Whop.api_attributes(result)
|
|
155
|
+
raise InvalidPayload, "Unexpected membership ID" unless object.fetch("id") == id
|
|
156
|
+
verify_resource!(object)
|
|
157
|
+
result
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def sync_membership(id)
|
|
161
|
+
verify_configuration!
|
|
162
|
+
Subscription.sync(id, account_id: account_id, customer: self)
|
|
163
|
+
end
|
|
164
|
+
|
|
140
165
|
def capture(payment_id, idempotency_key: SecureRandom.uuid)
|
|
141
166
|
payment(payment_id)
|
|
142
167
|
Whop.api.payments.capture(id: payment_id, request_options: Whop.request_options(idempotency_key: idempotency_key))
|
|
@@ -178,6 +203,10 @@ module Pay
|
|
|
178
203
|
verify_configuration!
|
|
179
204
|
raise Error, "Customer has not completed checkout" if processor_id.blank?
|
|
180
205
|
Whop.each_page(Whop.api.memberships, account_id: account_id, member_id: processor_id) do |object|
|
|
206
|
+
Whop.verify_account!(object, expected: account_id)
|
|
207
|
+
raise IdentityError, "Subscription list returned another member" unless object["member_id"] == processor_id
|
|
208
|
+
reference = self.class.find_signed(object.dig("metadata", "pay_customer"), purpose: Whop.identity_purpose(account_id))
|
|
209
|
+
next if Whop.allow_shared_members && reference && reference != self
|
|
181
210
|
verify_resource!(object)
|
|
182
211
|
Subscription.sync(object.fetch("id"), account_id: account_id)
|
|
183
212
|
end
|
|
@@ -193,7 +222,8 @@ module Pay
|
|
|
193
222
|
Whop.verify_account!(object, expected: account_id)
|
|
194
223
|
raise IdentityError, "Reconciliation returned another member" unless object["member_id"] == processor_id
|
|
195
224
|
token = object.dig("metadata", "pay_customer")
|
|
196
|
-
|
|
225
|
+
reference = token.present? ? self.class.find_signed(token, purpose: Whop.identity_purpose(account_id)) : nil
|
|
226
|
+
if (token.present? && reference.nil?) || (Whop.allow_shared_members && reference && reference != self)
|
|
197
227
|
counts[:skipped] += 1
|
|
198
228
|
ActiveSupport::Notifications.instrument("reconciliation_skipped.pay_whop", customer_id: id, resource_id: object.fetch("id"), resource_type: kind)
|
|
199
229
|
next
|
|
@@ -230,11 +260,12 @@ module Pay
|
|
|
230
260
|
token = object.dig("metadata", "pay_customer")
|
|
231
261
|
signed_customer = find_signed(token, purpose: Whop.identity_purpose(account)) if token.present?
|
|
232
262
|
raise IdentityError, "Invalid Pay customer reference" if token.present? && signed_customer.nil?
|
|
233
|
-
|
|
234
|
-
if
|
|
263
|
+
candidates = where(processor_id: member_id).select { |customer| customer.account_id == account && customer.data.to_h["whop_environment"] == Whop.environment }
|
|
264
|
+
if !Whop.allow_shared_members && signed_customer && candidates.any? { |customer| customer.id != signed_customer.id }
|
|
235
265
|
raise IdentityError, "Whop member is already linked to a different Pay customer"
|
|
236
266
|
end
|
|
237
|
-
customer
|
|
267
|
+
raise IdentityError, "Shared Whop members require a signed customer reference" if signed_customer.nil? && candidates.many?
|
|
268
|
+
customer = signed_customer || candidates.first
|
|
238
269
|
raise IdentityError, "Whop resource is not linked to a Pay customer" unless customer
|
|
239
270
|
customer.verify_configuration!
|
|
240
271
|
raise IdentityError, "Customer belongs to another seller" unless customer.account_id == account
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
module Pay
|
|
2
2
|
module Whop
|
|
3
3
|
class Subscription < Pay::Subscription
|
|
4
|
-
def self.sync(id, account_id: nil)
|
|
4
|
+
def self.sync(id, account_id: nil, customer: nil)
|
|
5
5
|
object = Whop.api_attributes(Whop.api.memberships.retrieve(id: id, request_options: Whop.request_options))
|
|
6
6
|
raise InvalidPayload, "Unexpected membership ID" unless object.fetch("id") == id
|
|
7
7
|
Whop.verify_account!(object, expected: account_id)
|
|
8
|
-
customer
|
|
8
|
+
customer&.verify_resource!(object)
|
|
9
|
+
customer ||= Customer.resolve!(object)
|
|
9
10
|
unless object["renewal_price"].present?
|
|
10
11
|
Entitlement.sync_object(object, account_id: customer.account_id)
|
|
11
12
|
return
|
|
@@ -54,6 +55,7 @@ module Pay
|
|
|
54
55
|
def paused? = data.to_h["whop_billing_paused"] == true
|
|
55
56
|
|
|
56
57
|
def active?
|
|
58
|
+
return on_trial? if status == "trialing"
|
|
57
59
|
return super unless paused?
|
|
58
60
|
%w[active trialing].include?(status) && current_period_end.present? && current_period_end.future? && (ends_at.nil? || ends_at.future?)
|
|
59
61
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class AllowSharedWhopMembers < ActiveRecord::Migration[7.0]
|
|
2
|
+
disable_ddl_transaction!
|
|
3
|
+
|
|
4
|
+
def up
|
|
5
|
+
add_index :pay_customers, [:processor, :processor_id], unique: true,
|
|
6
|
+
where: "processor <> 'whop'", name: "pay_non_whop_processor_identity", **index_options
|
|
7
|
+
add_index :pay_customers, [:processor, :processor_id],
|
|
8
|
+
where: "processor = 'whop'", name: "pay_whop_member_lookup", **index_options
|
|
9
|
+
remove_index :pay_customers, name: "index_pay_customers_on_processor_and_processor_id", **index_options
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def down
|
|
13
|
+
add_index :pay_customers, [:processor, :processor_id], unique: true,
|
|
14
|
+
name: "index_pay_customers_on_processor_and_processor_id", **index_options
|
|
15
|
+
remove_index :pay_customers, name: "pay_non_whop_processor_identity", **index_options
|
|
16
|
+
remove_index :pay_customers, name: "pay_whop_member_lookup", **index_options
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def index_options
|
|
22
|
+
(connection.adapter_name == "PostgreSQL") ? {algorithm: :concurrently} : {}
|
|
23
|
+
end
|
|
24
|
+
end
|
data/docs/advanced-usage.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
All amounts accepted by adapter convenience methods are integer minor units: `500` means USD 5.00. Specify `decimals: 0` for JPY or `decimals: 3` for KWD when creating prices or transfers; refund precision comes from the original payment. Arbitrary inline `variant` prices follow Whop's API and use major units.
|
|
4
4
|
|
|
5
|
+
Pay charge amounts use Whop's account-facing `total` and `refunded_amount`, rather than buyer fees. Currency mismatches are rejected. Adaptive pricing, FX refunds and tax reconciliation still require further validation.
|
|
6
|
+
|
|
5
7
|
## One-time and recurring checkout
|
|
6
8
|
|
|
7
9
|
```ruby
|
data/docs/demo.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Run the sandbox playground
|
|
2
|
+
|
|
3
|
+
From the repository root, using Ruby 3.3 or newer:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
bundle install
|
|
7
|
+
cp .env.example .env
|
|
8
|
+
# Fill in sandbox credentials, an existing recurring WHOP_VARIANT_ID,
|
|
9
|
+
# never commit .env.
|
|
10
|
+
bundle exec bin/sandbox-check
|
|
11
|
+
bundle exec examples/rails/bin/setup
|
|
12
|
+
cd examples/rails
|
|
13
|
+
bundle exec bin/rails server -b 127.0.0.1 -p 3001
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Open `http://localhost:3001` and start a sandbox session. Each browser gets its own synthetic customer in an encrypted session cookie; all billing actions are scoped to that customer. Reuse the same browser session with your Whop sandbox account. The public playground requires PostgreSQL and a secret key; its development secret key cannot be used in production. This example is not a production SaaS starter.
|
|
17
|
+
|
|
18
|
+
Whop requires an HTTPS checkout return URL. Set `WHOP_RETURN_URL` to an HTTPS page you control; for local testing it can be a static page that links back to localhost. Expose only the webhook route through your local tunnel, register that HTTPS URL in Whop, and set `WHOP_WEBHOOK_SECRET` before restarting the server. Checkout returns to the billing page; refresh after the webhook arrives. A successful subscription, cancellation and refund should be visible in both Pay and Whop.
|
|
19
|
+
|
|
20
|
+
Recover pending local deliveries with:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
bundle exec examples/rails/bin/reconcile
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Deploy the playground
|
|
27
|
+
|
|
28
|
+
`render.yaml` defines a free Rails web service. Supply a separate persistent PostgreSQL database (for example, Neon Free); Render’s free database expires after 30 days. The app uses GoodJob with PostgreSQL-backed jobs, bounded retries and a recovery job for pending deliveries. It only boots against Whop sandbox. Free hosting has usage limits and the web service sleeps when idle.
|
|
29
|
+
|
|
30
|
+
Set the listed environment variables in the host’s secret settings. Optionally set `DEMO_MERCHANT_ID` to a registered sandbox subaccount to show marketplace checkout, and `WHOP_ONE_TIME_VARIANT_ID` to a USD 5.00 one-time variant to show draft invoices. Set `WHOP_RETURN_URL` to the public HTTPS root URL, register `/pay/whop/webhooks` with Whop using the adapter’s event list and dated API version, then set `WHOP_WEBHOOK_SECRET`. `examples/rails/bin/start` applies migrations and starts Puma. Keep a stable `SECRET_KEY_BASE`: changing it invalidates sessions and the customer references already stored in Whop.
|
|
31
|
+
|
|
32
|
+
Never expose the local developer database or reuse a production Whop API key for this demo. The public app has no admin interface and never accepts visitor API keys.
|
|
33
|
+
|
|
34
|
+
## Screenshots
|
|
35
|
+
|
|
36
|
+
Repository screenshots were captured from the deployed sandbox on 12 November 2026. Billing screenshots show synthetic customers and Whop test payments; they contain no API keys, buyer contact details or live-money transactions. Tax can make a USD 5.00 checkout appear as USD 6.00 in the billing table. The amber invoice/transfer section uses separate local simulation records.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Public playground QA — 12 November 2026
|
|
2
|
+
|
|
3
|
+
Scope: https://pay-whop.onrender.com/, using the existing browser's synthetic sandbox customer and a newly completed one-time checkout. No live-money operations were performed.
|
|
4
|
+
|
|
5
|
+
| Check | Result |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| Checkout return / webhook | New payment appeared after refreshing; the return query alone did not create a charge or grant subscription access |
|
|
8
|
+
| Exact paid amount | Provider and Pay both recorded 599 USD minor units |
|
|
9
|
+
| Partial refund | The page requested USD 1.00; provider and Pay both reported exactly 100 minor units refunded |
|
|
10
|
+
| Saved methods | Refresh completed without an error and retained the member-scoped test card |
|
|
11
|
+
| Reconciliation | Completed without an error; the new payment remained a single row and its confirmed refund amount was preserved |
|
|
12
|
+
| Webhook backlog | Zero pending deliveries when checked after payment/refund |
|
|
13
|
+
| Invoice simulation | Reset → draft → issued → paid, with a new local activity history |
|
|
14
|
+
| Transfer simulation | Reset → pending → failed, with a new local activity history |
|
|
15
|
+
| Simulation isolation from billing | The payment table remained at six actual payment rows; simulation controls did not add a charge |
|
|
16
|
+
| Keyboard | Skip link and checkout-button navigation worked; focusable table regions and refund buttons were checked locally |
|
|
17
|
+
| Narrow layouts | Checked at 320px and 390px; no whole-page horizontal overflow. A billing readability issue was found and fixed |
|
|
18
|
+
| Regression suite | 100 tests / 369 assertions and Standard Ruby passed |
|
|
19
|
+
|
|
20
|
+
## Finding and correction
|
|
21
|
+
|
|
22
|
+
At 320px the tables technically fit the page, but IDs, dates, amounts and labels broke across many narrow lines. Billing tables now preserve readable columns in a locally scrollable region, with a visible small-screen hint and keyboard focus. The action column stacks buttons on mobile. Local browser checks confirmed that tabbing to refund controls scrolls them fully into view at 320px, with no page overflow; 390px was also checked.
|
|
23
|
+
|
|
24
|
+
The local UI check used a blocked-network preview with synthetic records. The deployed page was separately exercised against the actual sandbox before the layout correction, then rechecked after deployment.
|
|
25
|
+
|
|
26
|
+
## Limits of this pass
|
|
27
|
+
|
|
28
|
+
A fresh recurring checkout, new 3DS challenge, subscription pause/cancel/resume, real invoice issuance, internal transfer settlement and bank payout were not repeated in this pass. Existing evidence and provider limitations remain documented in [verification.md](verification.md). This is targeted QA, not a complete accessibility or production-readiness assessment.
|
data/docs/releasing.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Releasing
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The current published alpha is [`0.1.0.alpha.5`](https://rubygems.org/gems/pay-whop/versions/0.1.0.alpha.5). Its [GitHub prerelease](https://github.com/yshmarov/pay-whop/releases/tag/v0.1.0.alpha.5) contains the matching gem artifact.
|
|
4
4
|
|
|
5
|
-
1.
|
|
6
|
-
2.
|
|
7
|
-
3. Build with `gem build pay-whop.gemspec --output pkg/pay-whop-0.1.0.alpha.
|
|
8
|
-
4. Inspect `gem specification pkg/pay-whop-0.1.0.alpha.
|
|
9
|
-
5. A gem owner publishes with `gem push pkg/pay-whop-0.1.0.alpha.
|
|
5
|
+
1. Update the version, lockfile, changelog and verification evidence together.
|
|
6
|
+
2. Run `bin/test`, the PostgreSQL suite, and `bundle exec ruby bin/install-smoke`. Confirm the GitHub CI matrix is green.
|
|
7
|
+
3. Build with `gem build pay-whop.gemspec --output pkg/pay-whop-0.1.0.alpha.5.gem` (create `pkg` first).
|
|
8
|
+
4. Inspect `gem specification pkg/pay-whop-0.1.0.alpha.5.gem files` and ensure no credentials, private fixtures or developer environment files are packaged.
|
|
9
|
+
5. A gem owner publishes with `gem push pkg/pay-whop-0.1.0.alpha.5.gem`. RubyGems MFA is required by the gem metadata. Configure credentials locally; never commit or paste an API key.
|
|
10
10
|
6. Create the matching GitHub prerelease with the built artifact and release notes. Register the deployed adapter's current webhook event list only after deploying the compatible code.
|
|
11
11
|
|
|
12
12
|
Do not label this production-ready while real invoice issuance, transfers and bank payouts lack provider validation. Keep sandbox-only demo simulations visibly separate from real adapter behavior.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# One Whop buyer, multiple Pay customers
|
|
2
|
+
|
|
3
|
+
Some applications bill organizations while the same human pays for more than one organization. Enable this explicitly:
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
Pay::Whop.allow_shared_members = true
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The default remains `false`. Install and run the new `AllowSharedWhopMembers` engine migration first. It keeps Pay's processor/member uniqueness constraint for other processors and adds a nonunique Whop member lookup index.
|
|
10
|
+
|
|
11
|
+
With sharing enabled, signed checkout metadata selects the exact Pay customer. Both customers can retain the same provider member ID; neither customer's owner, seller or environment changes. Existing customer-to-member links remain immutable. When more than one matching customer exists, an event without a signed reference is rejected instead of choosing an organization. Invalid signatures never fall back to a member lookup.
|
|
12
|
+
|
|
13
|
+
Every checkout, payment and recurring membership must preserve the adapter's `pay_customer` metadata. Use separate Whop seller/account contexts for unrelated applications. Sharing a buyer within one application does not make signatures from another Rails application valid.
|
|
14
|
+
|
|
15
|
+
Rolling back the index migration fails if shared member mappings still exist. Resolve those mappings deliberately before rollback; the migration does not merge or delete customer history.
|
|
16
|
+
|
|
17
|
+
Tests cover default identity-conflict protection, opt-in sharing, ambiguous unsigned references, forged references and attempts to change an existing customer's provider member. Checkout and transfer contract tests also verify fractional amounts at the HTTP boundary.
|
data/docs/verification.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Verification
|
|
2
2
|
|
|
3
|
-
Provider payment-flow verification: 7 November 2026. Expanded adapter, read-only sandbox operations and demo verification: 12 November 2026. Release: `0.1.0.alpha.
|
|
3
|
+
Provider payment-flow verification: 7 November 2026. Expanded adapter, read-only sandbox operations and demo verification: 12 November 2026. Release: [`0.1.0.alpha.5` on RubyGems](https://rubygems.org/gems/pay-whop/versions/0.1.0.alpha.5). The registry SHA-256 matches the built gem tested by the installation smoke check.
|
|
4
4
|
|
|
5
5
|
## Versions and automated coverage
|
|
6
6
|
|
|
7
7
|
Ruby 4.0.9, Rails 8.1.4.1, Pay 12.4.0, Whop Ruby SDK 2.7.0, and Whop API date `2026-10-28-1`. The demo runs PostgreSQL and GoodJob on Render Free / Neon Free. Unit and integration tests use SQLite and PostgreSQL, with WebMock at the SDK/HTTP boundary. No test credentials or captured customer payloads are committed.
|
|
8
8
|
|
|
9
|
-
The local suite passes **
|
|
9
|
+
The local suite passes **109 tests / 423 assertions** and Standard Ruby linting. The local suite also passes with Pay 12.3.0; installation from the built gem into a fresh Rails application passes. GitHub Actions runs Ruby 3.3/3.4/4.0 × Pay 12.3.0/12.4.0 × SQLite/PostgreSQL; see [CI runs](https://github.com/yshmarov/pay-whop/actions/workflows/ci.yml).
|
|
10
10
|
|
|
11
11
|
Coverage includes signed owner mapping, environment and seller isolation, conflicting identities, forged references, webhook authentication/replays/collisions, duplicate processing, pending-event recovery, enqueue refusal, stale snapshots, exact money conversion, full/partial refunds, asynchronous direct payments, saved methods, setup intents, invoice/dispute summaries, marketplace registration and transfers, terminal transfer-state protection, pause/access semantics, trial and past-due state mapping, and browser-session/CSRF/cross-customer boundaries. Expanded tests cover existing-payment retries without duplicate charges, renewal restrictions, normalized API errors including lazy pagination/timeouts, application callback failure recovery, payout ownership/pagination/reversals, merchant capabilities, decimal balances and linked-customer reconciliation.
|
|
12
12
|
|
|
@@ -41,7 +41,7 @@ The [hosted playground](https://pay-whop.onrender.com/) uses the adapter and rec
|
|
|
41
41
|
|
|
42
42
|
A generated checkout or payout URL is not proof of settled payment or bank withdrawal. Completed provider authentication challenges, dispute submission and evidence upload, one-time entitlement reactivation, payment-method deletion, promo-code application, and full trial/failed-renewal lifecycles have HTTP-contract coverage only where exercised by tests, not complete real-provider coverage. Do not describe them as sandbox-certified.
|
|
43
43
|
|
|
44
|
-
The
|
|
44
|
+
The deployed webhook subscribes to all 30 adapter events, including payout changes and trial-ending notifications, with subaccount events enabled. No pending deliveries remained after the checkout and setup tests. The earlier provider-flow review covered desktop layout; a full mobile/accessibility assessment remains outstanding.
|
|
45
45
|
|
|
46
46
|
## Integration findings
|
|
47
47
|
|
|
@@ -56,6 +56,12 @@ The adapter event list now includes 30 events (payout changes and trial-ending n
|
|
|
56
56
|
9. Immediate cancellation can leave `cancel_at_period_end: true` on the provider object. Terminal status and `canceled_at` take precedence so ended subscriptions cannot retain access or appear resumable.
|
|
57
57
|
10. Refunds and transfers can fail due to available balance or account capabilities, even in the sandbox. The adapter does not bypass identity verification.
|
|
58
58
|
|
|
59
|
+
## PaidCollabs integration verification (13 November 2026)
|
|
60
|
+
|
|
61
|
+
Actual sandbox checks in a separate PaidCollabs application verified a signed hosted payment, duplicate webhook handling, full refund and the nested `payment.id` refund event. A real EUR 9.99 recurring membership started a 21-day trial with EUR 0 charged, then was canceled and verified ended. An actual PLN checkout configuration confirmed adaptive pricing disabled. These checks used an isolated database and existing sandbox credentials; no customer payloads or keys are packaged.
|
|
62
|
+
|
|
63
|
+
This release adds opt-in shared buyers with signed per-organization ownership, verified customer-scoped membership returns, local trial expiry even when webhooks are delayed, and preservation of explicit adaptive-pricing settings omitted by SDK 2.7. Successful internal transfers and bank settlement remain unverified; Whop sandbox payouts are unavailable. PaidCollabs renewal/dunning lifecycle tests use HTTP fixtures rather than waiting for a complete provider billing cycle.
|
|
64
|
+
|
|
59
65
|
## Remaining limits
|
|
60
66
|
|
|
61
67
|
This is an alpha integration, not a declaration of production readiness or full Pay/Stripe parity. Live-money processing, high-concurrency PostgreSQL races, full accessibility assessment, completed provider SCA recovery, FX/tax reconciliation, global historical imports, compatibility beyond the tested matrix and long-running renewal/retry behavior remain unverified. Renewal price/cadence/date mutations and bank payout transitions have HTTP-contract coverage; they were not validated by running a complete real provider billing cycle. Local browser checks confirmed the recovery URL/error state and new demo controls at desktop, 320px and 390px widths; the preview used blocked-network HTTP fixtures, not a completed Whop challenge.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Public sandbox QA preserves billing truth
|
|
2
|
+
|
|
3
|
+
**Outcome:** Check the buyer's completed purchase against Whop, then address the mobile issue found during hands-on QA.
|
|
4
|
+
|
|
5
|
+
- **Use provider confirmation.** A successful checkout return remains a hint to refresh, not proof of payment; the new payment and exact partial refund were checked against the provider and Pay.
|
|
6
|
+
- **Readability over squeezing.** On narrow screens, scrolling a readable table is preferable to fitting every column by breaking amounts and IDs into fragments. Keyboard users must be able to reach the controls too.
|
|
7
|
+
- **Keep the scope explicit.** This pass does not repeat verification-gated invoice/transfer settlement or certify production/accessibility conformance. PaidCollabs is a separate, reversible integration experiment rather than a production Stripe migration.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Credibility through inspectable evidence
|
|
2
|
+
|
|
3
|
+
**Outcome:** Present the adapter as a usable community alpha whose claims visitors can check.
|
|
4
|
+
|
|
5
|
+
- **Trust signals must be earned.** Link the published gem, real CI matrix, fresh-app installation evidence and dated sandbox observations. Avoid invented adoption, coverage percentages, audit claims or official endorsement.
|
|
6
|
+
- **Show the running product.** Use actual screenshots of the deployed Rails demo with synthetic billing records. Keep local simulations visibly distinct from provider-confirmed behavior.
|
|
7
|
+
- **Keep limitations discoverable.** Preserve alpha status, unsupported Pay/Stripe concepts and provider verification gaps rather than hiding them behind polished presentation.
|
|
8
|
+
- **Make participation practical.** Give contributors a reproducible local check and a private route for vulnerability reports, without promising response times or independent security certification.
|
data/lib/pay/whop/version.rb
CHANGED
data/lib/pay/whop.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pay-whop
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.0.alpha.
|
|
4
|
+
version: 0.1.0.alpha.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Shmarov
|
|
@@ -64,12 +64,18 @@ files:
|
|
|
64
64
|
- config/routes.rb
|
|
65
65
|
- db/migrate/20261107000000_create_pay_whop_deliveries.rb
|
|
66
66
|
- db/migrate/20261107120000_create_pay_whop_resources.rb
|
|
67
|
+
- db/migrate/20261113000000_allow_shared_whop_members.rb
|
|
67
68
|
- docs/advanced-usage.md
|
|
68
69
|
- docs/compatibility.md
|
|
70
|
+
- docs/demo.md
|
|
71
|
+
- docs/qa-2026-11-12.md
|
|
69
72
|
- docs/releasing.md
|
|
73
|
+
- docs/shared-buyers.md
|
|
70
74
|
- docs/verification.md
|
|
71
75
|
- docs/worklog/2026-11-12-demo-billing-simulations.md
|
|
76
|
+
- docs/worklog/2026-11-12-demo-qa.md
|
|
72
77
|
- docs/worklog/2026-11-12-integration-release.md
|
|
78
|
+
- docs/worklog/2026-11-12-repository-presentation.md
|
|
73
79
|
- lib/pay-whop.rb
|
|
74
80
|
- lib/pay/whop.rb
|
|
75
81
|
- lib/pay/whop/api_proxy.rb
|