From 91466a1af48c49ab745b3066c8eccc1ba8a26562 Mon Sep 17 00:00:00 2001 From: Talha Ijaz Date: Fri, 20 May 2022 00:41:46 +0500 Subject: [PATCH] first commit --- .env | 2 + .gitignore | 8 + .rspec | 1 + .travis.yml | 25 + CHANGELOG.md | 47 + Gemfile | 13 + LICENSE.txt | 22 + Rakefile | 14 + bin/stripe-mock-server | 19 + lib/stripe_mock.rb | 102 ++ lib/stripe_mock/api/account_balance.rb | 14 + lib/stripe_mock/api/bank_tokens.rb | 13 + lib/stripe_mock/api/card_tokens.rb | 13 + lib/stripe_mock/api/client.rb | 41 + lib/stripe_mock/api/conversion_rate.rb | 14 + lib/stripe_mock/api/debug.rb | 11 + lib/stripe_mock/api/errors.rb | 71 + lib/stripe_mock/api/global_id_prefix.rb | 22 + lib/stripe_mock/api/instance.rb | 38 + lib/stripe_mock/api/live.rb | 15 + lib/stripe_mock/api/server.rb | 39 + lib/stripe_mock/api/test_helpers.rb | 24 + lib/stripe_mock/api/webhooks.rb | 99 ++ lib/stripe_mock/client.rb | 128 ++ lib/stripe_mock/data.rb | 1388 ++++++++++++++++ lib/stripe_mock/data/list.rb | 106 ++ lib/stripe_mock/error_queue.rb | 27 + .../errors/closed_client_connection_error.rb | 9 + .../errors/server_timeout_error.rb | 12 + lib/stripe_mock/errors/stripe_mock_error.rb | 15 + .../errors/uninitialized_instance_error.rb | 9 + .../errors/unstarted_state_error.rb | 9 + .../errors/unsupported_request_error.rb | 4 + lib/stripe_mock/instance.rb | 248 +++ .../request_handlers/account_links.rb | 15 + lib/stripe_mock/request_handlers/accounts.rb | 86 + lib/stripe_mock/request_handlers/balance.rb | 17 + .../request_handlers/balance_transactions.rb | 37 + lib/stripe_mock/request_handlers/cards.rb | 35 + lib/stripe_mock/request_handlers/charges.rb | 184 +++ .../request_handlers/checkout_session.rb | 179 +++ .../request_handlers/country_spec.rb | 22 + lib/stripe_mock/request_handlers/coupons.rb | 35 + lib/stripe_mock/request_handlers/customers.rb | 154 ++ lib/stripe_mock/request_handlers/disputes.rb | 35 + .../request_handlers/ephemeral_key.rb | 13 + lib/stripe_mock/request_handlers/events.rb | 48 + .../request_handlers/express_login_links.rb | 15 + .../request_handlers/external_accounts.rb | 55 + .../helpers/bank_account_helpers.rb | 14 + .../request_handlers/helpers/card_helpers.rb | 127 ++ .../helpers/charge_helpers.rb | 16 + .../helpers/coupon_helpers.rb | 22 + .../helpers/external_account_helpers.rb | 49 + .../helpers/subscription_helpers.rb | 130 ++ .../request_handlers/helpers/token_helpers.rb | 44 + .../request_handlers/invoice_items.rb | 45 + lib/stripe_mock/request_handlers/invoices.rb | 183 +++ lib/stripe_mock/request_handlers/orders.rb | 80 + .../request_handlers/payment_intents.rb | 187 +++ .../request_handlers/payment_methods.rb | 124 ++ lib/stripe_mock/request_handlers/payouts.rb | 32 + lib/stripe_mock/request_handlers/plans.rb | 42 + lib/stripe_mock/request_handlers/prices.rb | 50 + lib/stripe_mock/request_handlers/products.rb | 44 + .../request_handlers/recipients.rb | 60 + lib/stripe_mock/request_handlers/refunds.rb | 102 ++ .../request_handlers/setup_intents.rb | 98 ++ lib/stripe_mock/request_handlers/sources.rb | 61 + .../request_handlers/subscription_items.rb | 36 + .../request_handlers/subscriptions.rb | 359 +++++ lib/stripe_mock/request_handlers/tax_rates.rb | 36 + lib/stripe_mock/request_handlers/tokens.rb | 77 + lib/stripe_mock/request_handlers/transfers.rb | 65 + .../validators/param_validators.rb | 147 ++ lib/stripe_mock/server.rb | 93 ++ lib/stripe_mock/test_strategies/base.rb | 167 ++ lib/stripe_mock/test_strategies/live.rb | 51 + lib/stripe_mock/test_strategies/mock.rb | 31 + lib/stripe_mock/util.rb | 44 + lib/stripe_mock/version.rb | 4 + .../account.application.deauthorized.json | 12 + .../account.external_account.created.json | 27 + .../account.external_account.deleted.json | 27 + .../account.external_account.updated.json | 27 + .../webhook_fixtures/account.updated.json | 26 + .../webhook_fixtures/balance.available.json | 31 + .../charge.dispute.closed.json | 22 + .../charge.dispute.created.json | 22 + .../charge.dispute.funds_reinstated.json | 88 ++ .../charge.dispute.funds_withdrawn.json | 88 ++ .../charge.dispute.updated.json | 25 + .../webhook_fixtures/charge.failed.json | 184 +++ .../webhook_fixtures/charge.refunded.json | 69 + .../webhook_fixtures/charge.succeeded.json | 55 + .../webhook_fixtures/charge.updated.json | 58 + ...eckout.session.completed.payment_mode.json | 53 + ...checkout.session.completed.setup_mode.json | 45 + .../webhook_fixtures/coupon.created.json | 23 + .../webhook_fixtures/coupon.deleted.json | 23 + .../webhook_fixtures/customer.created.json | 55 + .../webhook_fixtures/customer.deleted.json | 42 + .../customer.discount.created.json | 28 + .../customer.discount.deleted.json | 28 + .../customer.discount.updated.json | 43 + .../customer.source.created.json | 32 + .../customer.source.deleted.json | 32 + .../customer.source.updated.json | 36 + .../customer.subscription.created.json | 66 + .../customer.subscription.deleted.json | 65 + .../customer.subscription.trial_will_end.json | 65 + .../customer.subscription.updated.json | 78 + .../webhook_fixtures/customer.updated.json | 58 + .../webhook_fixtures/invoice.created.json | 71 + .../invoice.payment_failed.json | 105 ++ .../invoice.payment_succeeded.json | 112 ++ .../webhook_fixtures/invoice.upcoming.json | 70 + .../webhook_fixtures/invoice.updated.json | 74 + .../webhook_fixtures/invoiceitem.created.json | 21 + .../webhook_fixtures/invoiceitem.deleted.json | 21 + .../webhook_fixtures/invoiceitem.updated.json | 24 + .../webhook_fixtures/mandate.updated.json | 34 + .../payment_intent.canceled.json | 68 + .../payment_intent.payment_failed.json | 186 +++ .../payment_intent.processing.json | 162 ++ .../payment_intent.succeeded.json | 164 ++ .../webhook_fixtures/plan.created.json | 20 + .../webhook_fixtures/plan.deleted.json | 20 + .../webhook_fixtures/plan.updated.json | 23 + .../webhook_fixtures/product.created.json | 34 + .../webhook_fixtures/product.deleted.json | 34 + .../webhook_fixtures/product.updated.json | 38 + .../webhook_fixtures/transfer.created.json | 89 ++ .../webhook_fixtures/transfer.failed.json | 89 ++ .../webhook_fixtures/transfer.paid.json | 89 ++ .../webhook_fixtures/transfer.updated.json | 92 ++ lib/trollop.rb | 782 +++++++++ spec/_dummy/webhooks/dummy.event.json | 6 + spec/api/instance_spec.rb | 30 + spec/fixtures/create_refund.yml | 126 ++ .../stripe_webhooks/account.updated.json | 7 + .../custom.account.updated.json | 5 + spec/instance_spec.rb | 98 ++ .../charge_token_examples.rb | 51 + .../completing_checkout_sessions_example.rb | 37 + .../customer_card_examples.rb | 42 + .../prepare_error_examples.rb | 38 + spec/list_spec.rb | 178 +++ spec/readme_spec.rb | 75 + spec/server_spec.rb | 142 ++ .../account_examples.rb | 96 ++ .../account_link_examples.rb | 16 + .../balance_examples.rb | 17 + .../balance_transaction_examples.rb | 63 + spec/shared_stripe_examples/bank_examples.rb | 229 +++ .../bank_token_examples.rb | 59 + spec/shared_stripe_examples/card_examples.rb | 307 ++++ .../card_token_examples.rb | 181 +++ .../shared_stripe_examples/charge_examples.rb | 497 ++++++ .../checkout_session_examples.rb | 99 ++ .../country_specs_examples.rb | 18 + .../shared_stripe_examples/coupon_examples.rb | 85 + .../customer_examples.rb | 482 ++++++ .../dispute_examples.rb | 98 ++ .../ephemeral_key_examples.rb | 17 + .../error_mock_examples.rb | 163 ++ .../express_login_link_examples.rb | 12 + .../external_account_examples.rb | 170 ++ .../extra_features_examples.rb | 36 + .../invoice_examples.rb | 526 +++++++ .../invoice_item_examples.rb | 69 + .../payment_intent_examples.rb | 157 ++ .../payment_method_examples.rb | 454 ++++++ .../shared_stripe_examples/payout_examples.rb | 68 + spec/shared_stripe_examples/plan_examples.rb | 237 +++ spec/shared_stripe_examples/price_examples.rb | 183 +++ .../product_examples.rb | 147 ++ .../recipient_examples.rb | 118 ++ .../shared_stripe_examples/refund_examples.rb | 489 ++++++ .../setup_intent_examples.rb | 77 + .../subscription_examples.rb | 1399 +++++++++++++++++ .../subscription_items_examples.rb | 76 + .../tax_rate_examples.rb | 42 + .../transfer_examples.rb | 130 ++ .../validation_examples.rb | 19 + .../webhook_event_examples.rb | 307 ++++ spec/spec_helper.rb | 61 + spec/stripe_mock_spec.rb | 123 ++ .../shared_contexts/stripe_validator_spec.rb | 8 + spec/support/stripe_examples.rb | 49 + spec/util_spec.rb | 121 ++ stripe-ruby-mock.gemspec | 32 + 192 files changed, 18499 insertions(+) create mode 100644 .env create mode 100644 .gitignore create mode 100644 .rspec create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 Gemfile create mode 100644 LICENSE.txt create mode 100644 Rakefile create mode 100755 bin/stripe-mock-server create mode 100644 lib/stripe_mock.rb create mode 100644 lib/stripe_mock/api/account_balance.rb create mode 100644 lib/stripe_mock/api/bank_tokens.rb create mode 100644 lib/stripe_mock/api/card_tokens.rb create mode 100644 lib/stripe_mock/api/client.rb create mode 100644 lib/stripe_mock/api/conversion_rate.rb create mode 100644 lib/stripe_mock/api/debug.rb create mode 100644 lib/stripe_mock/api/errors.rb create mode 100644 lib/stripe_mock/api/global_id_prefix.rb create mode 100644 lib/stripe_mock/api/instance.rb create mode 100644 lib/stripe_mock/api/live.rb create mode 100644 lib/stripe_mock/api/server.rb create mode 100644 lib/stripe_mock/api/test_helpers.rb create mode 100644 lib/stripe_mock/api/webhooks.rb create mode 100644 lib/stripe_mock/client.rb create mode 100644 lib/stripe_mock/data.rb create mode 100644 lib/stripe_mock/data/list.rb create mode 100644 lib/stripe_mock/error_queue.rb create mode 100644 lib/stripe_mock/errors/closed_client_connection_error.rb create mode 100644 lib/stripe_mock/errors/server_timeout_error.rb create mode 100644 lib/stripe_mock/errors/stripe_mock_error.rb create mode 100644 lib/stripe_mock/errors/uninitialized_instance_error.rb create mode 100644 lib/stripe_mock/errors/unstarted_state_error.rb create mode 100644 lib/stripe_mock/errors/unsupported_request_error.rb create mode 100644 lib/stripe_mock/instance.rb create mode 100644 lib/stripe_mock/request_handlers/account_links.rb create mode 100644 lib/stripe_mock/request_handlers/accounts.rb create mode 100644 lib/stripe_mock/request_handlers/balance.rb create mode 100644 lib/stripe_mock/request_handlers/balance_transactions.rb create mode 100644 lib/stripe_mock/request_handlers/cards.rb create mode 100644 lib/stripe_mock/request_handlers/charges.rb create mode 100644 lib/stripe_mock/request_handlers/checkout_session.rb create mode 100644 lib/stripe_mock/request_handlers/country_spec.rb create mode 100644 lib/stripe_mock/request_handlers/coupons.rb create mode 100644 lib/stripe_mock/request_handlers/customers.rb create mode 100644 lib/stripe_mock/request_handlers/disputes.rb create mode 100644 lib/stripe_mock/request_handlers/ephemeral_key.rb create mode 100644 lib/stripe_mock/request_handlers/events.rb create mode 100644 lib/stripe_mock/request_handlers/express_login_links.rb create mode 100644 lib/stripe_mock/request_handlers/external_accounts.rb create mode 100644 lib/stripe_mock/request_handlers/helpers/bank_account_helpers.rb create mode 100644 lib/stripe_mock/request_handlers/helpers/card_helpers.rb create mode 100644 lib/stripe_mock/request_handlers/helpers/charge_helpers.rb create mode 100644 lib/stripe_mock/request_handlers/helpers/coupon_helpers.rb create mode 100644 lib/stripe_mock/request_handlers/helpers/external_account_helpers.rb create mode 100644 lib/stripe_mock/request_handlers/helpers/subscription_helpers.rb create mode 100644 lib/stripe_mock/request_handlers/helpers/token_helpers.rb create mode 100644 lib/stripe_mock/request_handlers/invoice_items.rb create mode 100644 lib/stripe_mock/request_handlers/invoices.rb create mode 100644 lib/stripe_mock/request_handlers/orders.rb create mode 100644 lib/stripe_mock/request_handlers/payment_intents.rb create mode 100644 lib/stripe_mock/request_handlers/payment_methods.rb create mode 100644 lib/stripe_mock/request_handlers/payouts.rb create mode 100644 lib/stripe_mock/request_handlers/plans.rb create mode 100644 lib/stripe_mock/request_handlers/prices.rb create mode 100644 lib/stripe_mock/request_handlers/products.rb create mode 100644 lib/stripe_mock/request_handlers/recipients.rb create mode 100644 lib/stripe_mock/request_handlers/refunds.rb create mode 100644 lib/stripe_mock/request_handlers/setup_intents.rb create mode 100644 lib/stripe_mock/request_handlers/sources.rb create mode 100644 lib/stripe_mock/request_handlers/subscription_items.rb create mode 100644 lib/stripe_mock/request_handlers/subscriptions.rb create mode 100644 lib/stripe_mock/request_handlers/tax_rates.rb create mode 100644 lib/stripe_mock/request_handlers/tokens.rb create mode 100644 lib/stripe_mock/request_handlers/transfers.rb create mode 100644 lib/stripe_mock/request_handlers/validators/param_validators.rb create mode 100644 lib/stripe_mock/server.rb create mode 100644 lib/stripe_mock/test_strategies/base.rb create mode 100644 lib/stripe_mock/test_strategies/live.rb create mode 100644 lib/stripe_mock/test_strategies/mock.rb create mode 100644 lib/stripe_mock/util.rb create mode 100644 lib/stripe_mock/version.rb create mode 100644 lib/stripe_mock/webhook_fixtures/account.application.deauthorized.json create mode 100644 lib/stripe_mock/webhook_fixtures/account.external_account.created.json create mode 100644 lib/stripe_mock/webhook_fixtures/account.external_account.deleted.json create mode 100644 lib/stripe_mock/webhook_fixtures/account.external_account.updated.json create mode 100644 lib/stripe_mock/webhook_fixtures/account.updated.json create mode 100644 lib/stripe_mock/webhook_fixtures/balance.available.json create mode 100644 lib/stripe_mock/webhook_fixtures/charge.dispute.closed.json create mode 100644 lib/stripe_mock/webhook_fixtures/charge.dispute.created.json create mode 100644 lib/stripe_mock/webhook_fixtures/charge.dispute.funds_reinstated.json create mode 100644 lib/stripe_mock/webhook_fixtures/charge.dispute.funds_withdrawn.json create mode 100644 lib/stripe_mock/webhook_fixtures/charge.dispute.updated.json create mode 100644 lib/stripe_mock/webhook_fixtures/charge.failed.json create mode 100644 lib/stripe_mock/webhook_fixtures/charge.refunded.json create mode 100644 lib/stripe_mock/webhook_fixtures/charge.succeeded.json create mode 100644 lib/stripe_mock/webhook_fixtures/charge.updated.json create mode 100644 lib/stripe_mock/webhook_fixtures/checkout.session.completed.payment_mode.json create mode 100644 lib/stripe_mock/webhook_fixtures/checkout.session.completed.setup_mode.json create mode 100644 lib/stripe_mock/webhook_fixtures/coupon.created.json create mode 100644 lib/stripe_mock/webhook_fixtures/coupon.deleted.json create mode 100644 lib/stripe_mock/webhook_fixtures/customer.created.json create mode 100644 lib/stripe_mock/webhook_fixtures/customer.deleted.json create mode 100644 lib/stripe_mock/webhook_fixtures/customer.discount.created.json create mode 100644 lib/stripe_mock/webhook_fixtures/customer.discount.deleted.json create mode 100644 lib/stripe_mock/webhook_fixtures/customer.discount.updated.json create mode 100644 lib/stripe_mock/webhook_fixtures/customer.source.created.json create mode 100644 lib/stripe_mock/webhook_fixtures/customer.source.deleted.json create mode 100644 lib/stripe_mock/webhook_fixtures/customer.source.updated.json create mode 100644 lib/stripe_mock/webhook_fixtures/customer.subscription.created.json create mode 100644 lib/stripe_mock/webhook_fixtures/customer.subscription.deleted.json create mode 100644 lib/stripe_mock/webhook_fixtures/customer.subscription.trial_will_end.json create mode 100644 lib/stripe_mock/webhook_fixtures/customer.subscription.updated.json create mode 100644 lib/stripe_mock/webhook_fixtures/customer.updated.json create mode 100644 lib/stripe_mock/webhook_fixtures/invoice.created.json create mode 100644 lib/stripe_mock/webhook_fixtures/invoice.payment_failed.json create mode 100644 lib/stripe_mock/webhook_fixtures/invoice.payment_succeeded.json create mode 100644 lib/stripe_mock/webhook_fixtures/invoice.upcoming.json create mode 100644 lib/stripe_mock/webhook_fixtures/invoice.updated.json create mode 100644 lib/stripe_mock/webhook_fixtures/invoiceitem.created.json create mode 100644 lib/stripe_mock/webhook_fixtures/invoiceitem.deleted.json create mode 100644 lib/stripe_mock/webhook_fixtures/invoiceitem.updated.json create mode 100644 lib/stripe_mock/webhook_fixtures/mandate.updated.json create mode 100644 lib/stripe_mock/webhook_fixtures/payment_intent.canceled.json create mode 100644 lib/stripe_mock/webhook_fixtures/payment_intent.payment_failed.json create mode 100644 lib/stripe_mock/webhook_fixtures/payment_intent.processing.json create mode 100644 lib/stripe_mock/webhook_fixtures/payment_intent.succeeded.json create mode 100644 lib/stripe_mock/webhook_fixtures/plan.created.json create mode 100644 lib/stripe_mock/webhook_fixtures/plan.deleted.json create mode 100644 lib/stripe_mock/webhook_fixtures/plan.updated.json create mode 100644 lib/stripe_mock/webhook_fixtures/product.created.json create mode 100644 lib/stripe_mock/webhook_fixtures/product.deleted.json create mode 100644 lib/stripe_mock/webhook_fixtures/product.updated.json create mode 100644 lib/stripe_mock/webhook_fixtures/transfer.created.json create mode 100644 lib/stripe_mock/webhook_fixtures/transfer.failed.json create mode 100644 lib/stripe_mock/webhook_fixtures/transfer.paid.json create mode 100644 lib/stripe_mock/webhook_fixtures/transfer.updated.json create mode 100644 lib/trollop.rb create mode 100644 spec/_dummy/webhooks/dummy.event.json create mode 100644 spec/api/instance_spec.rb create mode 100644 spec/fixtures/create_refund.yml create mode 100644 spec/fixtures/stripe_webhooks/account.updated.json create mode 100644 spec/fixtures/stripe_webhooks/custom.account.updated.json create mode 100644 spec/instance_spec.rb create mode 100644 spec/integration_examples/charge_token_examples.rb create mode 100644 spec/integration_examples/completing_checkout_sessions_example.rb create mode 100644 spec/integration_examples/customer_card_examples.rb create mode 100644 spec/integration_examples/prepare_error_examples.rb create mode 100644 spec/list_spec.rb create mode 100644 spec/readme_spec.rb create mode 100644 spec/server_spec.rb create mode 100644 spec/shared_stripe_examples/account_examples.rb create mode 100644 spec/shared_stripe_examples/account_link_examples.rb create mode 100644 spec/shared_stripe_examples/balance_examples.rb create mode 100644 spec/shared_stripe_examples/balance_transaction_examples.rb create mode 100644 spec/shared_stripe_examples/bank_examples.rb create mode 100644 spec/shared_stripe_examples/bank_token_examples.rb create mode 100644 spec/shared_stripe_examples/card_examples.rb create mode 100644 spec/shared_stripe_examples/card_token_examples.rb create mode 100644 spec/shared_stripe_examples/charge_examples.rb create mode 100644 spec/shared_stripe_examples/checkout_session_examples.rb create mode 100644 spec/shared_stripe_examples/country_specs_examples.rb create mode 100644 spec/shared_stripe_examples/coupon_examples.rb create mode 100644 spec/shared_stripe_examples/customer_examples.rb create mode 100644 spec/shared_stripe_examples/dispute_examples.rb create mode 100644 spec/shared_stripe_examples/ephemeral_key_examples.rb create mode 100644 spec/shared_stripe_examples/error_mock_examples.rb create mode 100644 spec/shared_stripe_examples/express_login_link_examples.rb create mode 100644 spec/shared_stripe_examples/external_account_examples.rb create mode 100644 spec/shared_stripe_examples/extra_features_examples.rb create mode 100644 spec/shared_stripe_examples/invoice_examples.rb create mode 100644 spec/shared_stripe_examples/invoice_item_examples.rb create mode 100644 spec/shared_stripe_examples/payment_intent_examples.rb create mode 100644 spec/shared_stripe_examples/payment_method_examples.rb create mode 100644 spec/shared_stripe_examples/payout_examples.rb create mode 100644 spec/shared_stripe_examples/plan_examples.rb create mode 100644 spec/shared_stripe_examples/price_examples.rb create mode 100644 spec/shared_stripe_examples/product_examples.rb create mode 100644 spec/shared_stripe_examples/recipient_examples.rb create mode 100644 spec/shared_stripe_examples/refund_examples.rb create mode 100644 spec/shared_stripe_examples/setup_intent_examples.rb create mode 100644 spec/shared_stripe_examples/subscription_examples.rb create mode 100644 spec/shared_stripe_examples/subscription_items_examples.rb create mode 100644 spec/shared_stripe_examples/tax_rate_examples.rb create mode 100644 spec/shared_stripe_examples/transfer_examples.rb create mode 100644 spec/shared_stripe_examples/validation_examples.rb create mode 100644 spec/shared_stripe_examples/webhook_event_examples.rb create mode 100644 spec/spec_helper.rb create mode 100644 spec/stripe_mock_spec.rb create mode 100644 spec/support/shared_contexts/stripe_validator_spec.rb create mode 100644 spec/support/stripe_examples.rb create mode 100644 spec/util_spec.rb create mode 100644 stripe-ruby-mock.gemspec diff --git a/.env b/.env new file mode 100644 index 0000000..843558d --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +STRIPE_TEST_SECRET_KEY=sk_test_eFvAvN5rz4GqAbsWxg63Jx79 +STRIPE_TEST_OAUTH_ACCESS_TOKEN=sk_test_WnZmEBIHhMcDltNe98sqWN7z \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c053d63 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +html/ +pkg/ +.DS_Store +stripe-mock-server.pid +Gemfile.lock +stripe-mock-server.log +.idea +.ruby-version diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..660778b --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--colour --format documentation diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e209a0c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,25 @@ +sudo: required +language: ruby +rvm: + - 2.4.6 + - 2.5.5 + - 2.6.3 + - 2.7.0 +before_install: + - gem install bundler -v '< 2' +before_script: + - "sudo touch /var/log/stripe-mock-server.log" + - "sudo chown travis /var/log/stripe-mock-server.log" +script: "bundle exec rspec && bundle exec rspec -t live" + +env: + global: + - IS_TRAVIS=true STRIPE_TEST_SECRET_KEY_A=sk_test_BsztzqQjzd7lqkgo1LjEG5DF00KzH7tWKF STRIPE_TEST_SECRET_KEY_B=sk_test_rKCEu0x8jzg6cKPqoey8kUPQ00usQO3KYE STRIPE_TEST_SECRET_KEY_C=sk_test_qeaB7R6Ywp8sC9pzd1ZIABH700YLC7nhmZ STRIPE_TEST_SECRET_KEY_D=sk_test_r1NwHkUW7UyoozyP4aEBD6cs00CI5uDiGq + +notifications: + webhooks: + urls: + - https://webhooks.gitter.im/e/44a1f4718ae2efb67eac + on_success: change # options: [always|never|change] default: always + on_failure: always # options: [always|never|change] default: always + on_start: false # default: false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f76a14f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,47 @@ +### Unreleased + +- [#806](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/806) - Remove `payment_method_types` from required arguments for `Stripe::Checkout::Session` +- [#806](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/806) - Raise more helpful exception when Stripe::Price cannot be found within a `Stripe::Checkout::Session` `line_items` argument. + + +### 3.1.0.rc3 (pre-release 2021-07-14) + +- [#785](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/785): `Stripe::Product` no longer requires `type`. [@TastyPi](https://github.com/TastyPi) +- [#784](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/784): Fix "Wrong number of arguments" error in tests. [@TastyPi](https://github.com/TastyPi) +- [#782](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/782): Support expanding `setup_intent` in `Stripe::Checkout::Session`. [@TastyPi](https://github.com/TastyPi) + +### 3.1.0.rc2 (pre-release 2021-03-03) + +- [#767](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/767): Fixes tests and more [@lpsBetty](https://github.com/lpsBetty) + +### 3.1.0.rc1 (pre-release 2021-02-17) + +- [#765](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/765): Properly set the status of a trialing subscription. [@csalvato](https://github.com/csalvato) +- [#764](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/764): Fixes erroneous error message when fetching upcoming invoices. [@csalvato](https://github.com/csalvato) +- [#762](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/762): Support Stripe Connect with Customers by adding stripe_account header namespace for customer object [@csalvato](https://github.com/csalvato) +- [#755](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/755): Add allowed params to subscriptions [@dominikdarnel ](https://github.com/dominikdarnel) +- [#748](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/758): Support Prices - [@hidenba](https://github.com/hidenba) and [@jamesprior](https://github.com/jamesprior). +- [#747](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/747/files): Fix ruby 2.7 deprecation warnings. Adds Ruby 3.0.0 compatibility. [@coding-chimp](https://github.com/coding-chimp) +- [#715](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/715): Added application_fee_amount to mock charge object - [@espen](https://github.com/espen) +- [#709](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/709): Remove unnecessary check on customer's currency - [@coorasse](https://github.com/coorasse) + +### 3.0.1 (TBD) + +- Added Changelog file +- [#640](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/640): Support Payment Intent status requires_capture - [@theodorton](https://github.com/theodorton). +- [#685](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/685): Adds support for pending_invoice_item_interval - [@joshcass](https://github.com/joshcass). +- [#682](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/682): Prevent customer metadata from being overwritten with each update - [@sethkrasnianski](https://github.com/sethkrasnianski). +- [#679](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/679): Fix for [#678](https://github.com/stripe-ruby-mock/stripe-ruby-mock/issues/678) Add active filter to Data::List - [@rnmp](https://github.com/rnmp). +- [#668](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/668): Fix for [#665](https://github.com/stripe-ruby-mock/stripe-ruby-mock/issues/665) Allow to remove discount from customer - [@mnin](https://github.com/mnin). +- [#667](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/667): + Remove empty and duplicated methods from payment methods - [@mnin](https://github.com/mnin). +- [#664](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/664): Bugfix: pass through PaymentIntent amount to mocked Charge - [@typeoneerror](https://github.com/typeoneerror). +- [#654](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/654): fix for [#626](https://github.com/stripe-ruby-mock/stripe-ruby-mock/issues/626) Added missing decline codes - [@iCreateJB](https://github.com/iCreateJB). +- [#648](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/648): Initial implementation of checkout session API - [@fauxparse](https://github.com/fauxparse). +- [#644](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/644): Allow payment_behavior attribute on subscription create - [@j15e](https://github.com/j15e). + +### 3.0.0 (2019-12-17) + +##### the main thing is: + +- [#658](https://github.com/stripe-ruby-mock/stripe-ruby-mock/pull/658) Make the gem compatible with Stripe Gem v.5 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..8cc610a --- /dev/null +++ b/Gemfile @@ -0,0 +1,13 @@ +source 'https://rubygems.org' + +platforms :ruby_19 do + gem 'mime-types', '~> 2.6' + gem 'rest-client', '~> 1.8' +end + +group :test do + gem 'rake' + gem 'dotenv' +end + +gemspec diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..1d13106 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2013 Gilbert + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..144d19b --- /dev/null +++ b/Rakefile @@ -0,0 +1,14 @@ +# encoding: utf-8 + +require 'rubygems' +require 'rake' + +begin + gem 'rubygems-tasks', '~> 0.2' + require 'rubygems/tasks' + + Gem::Tasks.new +rescue LoadError => e + warn e.message + warn "Run `gem install rubygems-tasks` to install Gem::Tasks." +end diff --git a/bin/stripe-mock-server b/bin/stripe-mock-server new file mode 100755 index 0000000..11b6769 --- /dev/null +++ b/bin/stripe-mock-server @@ -0,0 +1,19 @@ +#!/usr/bin/env ruby + +lib = File.expand_path(File.dirname(__FILE__) + '/../lib') +$LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib) + +require 'trollop' + +opts = Trollop::options do + opt :port, "Listening port", :type => :int, :default => 4999 + opt :host, "Host to listen on", :type => :string, :default => '0.0.0.0' + opt :server, "Server to use", :type => :string, :default => 'thin' + opt :debug, "Request and response output", :default => true + opt :pid_path, "Location to put server pid file", :type => :string, :default => './stripe-mock-server.pid' +end + +require 'stripe_mock' +require 'stripe_mock/server' + +StripeMock::Server.start_new(opts) diff --git a/lib/stripe_mock.rb b/lib/stripe_mock.rb new file mode 100644 index 0000000..543ae1b --- /dev/null +++ b/lib/stripe_mock.rb @@ -0,0 +1,102 @@ +require 'ostruct' +require 'multi_json' +require 'dante' +require 'time' + +require 'stripe' + +require 'stripe_mock/version' +require 'stripe_mock/util' +require 'stripe_mock/error_queue' + +require 'stripe_mock/data' +require 'stripe_mock/data/list' + +require 'stripe_mock/errors/stripe_mock_error' +require 'stripe_mock/errors/unsupported_request_error' +require 'stripe_mock/errors/uninitialized_instance_error' +require 'stripe_mock/errors/unstarted_state_error' +require 'stripe_mock/errors/server_timeout_error' +require 'stripe_mock/errors/closed_client_connection_error' + +require 'stripe_mock/client' +require 'stripe_mock/server' + +require 'stripe_mock/api/instance' +require 'stripe_mock/api/client' +require 'stripe_mock/api/server' + +require 'stripe_mock/api/bank_tokens' +require 'stripe_mock/api/account_balance' +require 'stripe_mock/api/conversion_rate' +require 'stripe_mock/api/card_tokens' +require 'stripe_mock/api/debug' +require 'stripe_mock/api/errors' +require 'stripe_mock/api/global_id_prefix' +require 'stripe_mock/api/live' +require 'stripe_mock/api/test_helpers' +require 'stripe_mock/api/webhooks' + +require 'stripe_mock/request_handlers/helpers/bank_account_helpers.rb' +require 'stripe_mock/request_handlers/helpers/external_account_helpers.rb' +require 'stripe_mock/request_handlers/helpers/card_helpers.rb' +require 'stripe_mock/request_handlers/helpers/charge_helpers.rb' +require 'stripe_mock/request_handlers/helpers/coupon_helpers.rb' +require 'stripe_mock/request_handlers/helpers/subscription_helpers.rb' +require 'stripe_mock/request_handlers/helpers/token_helpers.rb' + +require 'stripe_mock/request_handlers/validators/param_validators.rb' + +require 'stripe_mock/request_handlers/account_links.rb' +require 'stripe_mock/request_handlers/express_login_links.rb' +require 'stripe_mock/request_handlers/accounts.rb' +require 'stripe_mock/request_handlers/external_accounts.rb' +require 'stripe_mock/request_handlers/balance.rb' +require 'stripe_mock/request_handlers/balance_transactions.rb' +require 'stripe_mock/request_handlers/charges.rb' +require 'stripe_mock/request_handlers/cards.rb' +require 'stripe_mock/request_handlers/sources.rb' +require 'stripe_mock/request_handlers/customers.rb' +require 'stripe_mock/request_handlers/coupons.rb' +require 'stripe_mock/request_handlers/disputes.rb' +require 'stripe_mock/request_handlers/events.rb' +require 'stripe_mock/request_handlers/invoices.rb' +require 'stripe_mock/request_handlers/invoice_items.rb' +require 'stripe_mock/request_handlers/orders.rb' +require 'stripe_mock/request_handlers/plans.rb' +require 'stripe_mock/request_handlers/prices.rb' +require 'stripe_mock/request_handlers/recipients.rb' +require 'stripe_mock/request_handlers/refunds.rb' +require 'stripe_mock/request_handlers/transfers.rb' +require 'stripe_mock/request_handlers/payment_intents.rb' +require 'stripe_mock/request_handlers/payment_methods.rb' +require 'stripe_mock/request_handlers/setup_intents.rb' +require 'stripe_mock/request_handlers/payouts.rb' +require 'stripe_mock/request_handlers/subscriptions.rb' +require 'stripe_mock/request_handlers/subscription_items.rb' +require 'stripe_mock/request_handlers/tokens.rb' +require 'stripe_mock/request_handlers/country_spec.rb' +require 'stripe_mock/request_handlers/ephemeral_key.rb' +require 'stripe_mock/request_handlers/products.rb' +require 'stripe_mock/request_handlers/tax_rates.rb' +require 'stripe_mock/request_handlers/checkout_session.rb' +require 'stripe_mock/instance' + +require 'stripe_mock/test_strategies/base.rb' +require 'stripe_mock/test_strategies/mock.rb' +require 'stripe_mock/test_strategies/live.rb' + +module StripeMock + + @default_currency = 'usd' + @checkout_base = "https://checkout.stripe.com/pay/" + lib_dir = File.expand_path(File.dirname(__FILE__), '../..') + @webhook_fixture_path = './spec/fixtures/stripe_webhooks/' + @webhook_fixture_fallback_path = File.join(lib_dir, 'stripe_mock/webhook_fixtures') + + class << self + attr_accessor :default_currency + attr_accessor :checkout_base + attr_accessor :webhook_fixture_path + end +end diff --git a/lib/stripe_mock/api/account_balance.rb b/lib/stripe_mock/api/account_balance.rb new file mode 100644 index 0000000..d09ddaa --- /dev/null +++ b/lib/stripe_mock/api/account_balance.rb @@ -0,0 +1,14 @@ +module StripeMock + + def self.set_account_balance(value) + case @state + when 'local' + instance.account_balance = value + when 'remote' + client.set_account_balance(value) + else + raise UnstartedStateError + end + end + +end diff --git a/lib/stripe_mock/api/bank_tokens.rb b/lib/stripe_mock/api/bank_tokens.rb new file mode 100644 index 0000000..00f2b0f --- /dev/null +++ b/lib/stripe_mock/api/bank_tokens.rb @@ -0,0 +1,13 @@ +module StripeMock + + def self.generate_bank_token(bank_params = {}) + case @state + when 'local' + instance.generate_bank_token(bank_params) + when 'remote' + client.generate_bank_token(bank_params) + else + raise UnstartedStateError + end + end +end diff --git a/lib/stripe_mock/api/card_tokens.rb b/lib/stripe_mock/api/card_tokens.rb new file mode 100644 index 0000000..6a842dd --- /dev/null +++ b/lib/stripe_mock/api/card_tokens.rb @@ -0,0 +1,13 @@ +module StripeMock + + def self.generate_card_token(card_params = {}) + case @state + when 'local' + instance.generate_card_token(card_params) + when 'remote' + client.generate_card_token(card_params) + else + raise UnstartedStateError + end + end +end diff --git a/lib/stripe_mock/api/client.rb b/lib/stripe_mock/api/client.rb new file mode 100644 index 0000000..08a14f6 --- /dev/null +++ b/lib/stripe_mock/api/client.rb @@ -0,0 +1,41 @@ +module StripeMock + + def self.client + @client + end + + def self.start_client(port=4999) + return false if @state == 'live' + return @client unless @client.nil? + + Stripe::StripeClient.send(:define_method, :execute_request) { |*args, **keyword_args| StripeMock.redirect_to_mock_server(*args, **keyword_args) } + @client = StripeMock::Client.new(port) + @state = 'remote' + @client + end + + def self.stop_client(opts={}) + return false unless @state == 'remote' + @state = 'ready' + + restore_stripe_execute_request_method + @client.clear_server_data if opts[:clear_server_data] == true + @client.cleanup + @client = nil + true + end + + private + + def self.redirect_to_mock_server(method, url, api_key: nil, api_base: nil, params: {}, headers: {}) + handler = Instance.handler_for_method_url("#{method} #{url}") + + if mock_error = client.error_queue.error_for_handler_name(handler[:name]) + client.error_queue.dequeue + raise mock_error + end + + Stripe::Util.symbolize_names client.mock_request(method, url, api_key: api_key, params: params, headers: headers) + end + +end diff --git a/lib/stripe_mock/api/conversion_rate.rb b/lib/stripe_mock/api/conversion_rate.rb new file mode 100644 index 0000000..aeb5409 --- /dev/null +++ b/lib/stripe_mock/api/conversion_rate.rb @@ -0,0 +1,14 @@ +module StripeMock + + def self.set_conversion_rate(value) + case @state + when 'local' + instance.conversion_rate = value + when 'remote' + client.set_conversion_rate(value) + else + raise UnstartedStateError + end + end + +end diff --git a/lib/stripe_mock/api/debug.rb b/lib/stripe_mock/api/debug.rb new file mode 100644 index 0000000..184e934 --- /dev/null +++ b/lib/stripe_mock/api/debug.rb @@ -0,0 +1,11 @@ +module StripeMock + + def self.toggle_debug(toggle) + if @state == 'local' + @instance.debug = toggle + elsif @state == 'remote' + @client.set_server_debug(toggle) + end + end + +end diff --git a/lib/stripe_mock/api/errors.rb b/lib/stripe_mock/api/errors.rb new file mode 100644 index 0000000..97f987b --- /dev/null +++ b/lib/stripe_mock/api/errors.rb @@ -0,0 +1,71 @@ +module StripeMock + def self.prepare_error(stripe_error, *handler_names) + handler_names.push(:all) if handler_names.count == 0 + + if @state == 'local' + instance + elsif @state == 'remote' + client + else + raise UnstartedStateError + end.error_queue.queue stripe_error, handler_names + end + + def self.prepare_card_error(code, *handler_names) + handler_names.push(:new_charge) if handler_names.count == 0 + + error = CardErrors.build_error_for(code) + if error.nil? + raise StripeMockError, "Unrecognized stripe card error code: #{code}" + end + + prepare_error error, *handler_names + end + + module CardErrors + def self.build_error_for(code) + case code + when :incorrect_number then build_card_error('The card number is incorrect', 'number', code: 'incorrect_number', http_status: 402) + when :invalid_number then build_card_error('The card number is not a valid credit card number', 'number', code: 'invalid_number', http_status: 402) + when :invalid_expiry_month then build_card_error("The card's expiration month is invalid", 'exp_month', code: 'invalid_expiry_month', http_status: 402) + when :invalid_expiry_year then build_card_error("The card's expiration year is invalid", 'exp_year', code: 'invalid_expiry_year', http_status: 402) + when :invalid_cvc then build_card_error("The card's security code is invalid", 'cvc', code: 'invalid_cvc', http_status: 402) + when :expired_card then build_card_error('The card has expired', 'exp_month', code: 'expired_card', http_status: 402) + when :incorrect_cvc then build_card_error("The card's security code is incorrect", 'cvc', code: 'incorrect_cvc', http_status: 402) + when :card_declined then build_card_error('The card was declined', nil, code: 'card_declined', http_status: 402) + when :missing then build_card_error('There is no card on a customer that is being charged.', nil, code: 'missing', http_status: 402) + when :processing_error then build_card_error('An error occurred while processing the card', nil, code: 'processing_error', http_status: 402) + when :card_error then build_card_error('The card number is not a valid credit card number.', 'number', code: 'invalid_number', http_status: 402) + when :incorrect_zip then build_card_error('The zip code you supplied failed validation.', 'address_zip', code: 'incorrect_zip', http_status: 402) + when :insufficient_funds then build_card_error('The card has insufficient funds to complete the purchase.', nil, code: 'insufficient_funds', http_status: 402) + when :lost_card then build_card_error('The payment has been declined because the card is reported lost.', nil, code: 'lost_card', http_status: 402) + when :stolen_card then build_card_error('The payment has been declined because the card is reported stolen.', nil, code: 'stolen_card', http_status: 402) + end + end + + def self.get_decline_code(code) + decline_code_map = { + card_declined: 'do_not_honor', + missing: nil + } + decline_code_map.default = code.to_s + + code_key = code.to_sym + decline_code_map[code_key] + end + + def self.build_card_error(message, param, **kwargs) + json_hash = { + message: message, + param: param, + code: kwargs[:code], + type: 'card_error', + decline_code: get_decline_code(kwargs[:code]) + } + + error_keyword_args = kwargs.merge(json_body: { error: json_hash }, http_body: { error: json_hash }.to_json) + + Stripe::CardError.new(message, param, **error_keyword_args) + end + end +end diff --git a/lib/stripe_mock/api/global_id_prefix.rb b/lib/stripe_mock/api/global_id_prefix.rb new file mode 100644 index 0000000..dcaf3fe --- /dev/null +++ b/lib/stripe_mock/api/global_id_prefix.rb @@ -0,0 +1,22 @@ +module StripeMock + + def self.global_id_prefix + if StripeMock.client + StripeMock.client.server_global_id_prefix + else + case @global_id_prefix + when false then "" + when nil then "test_" + else @global_id_prefix + end + end + end + + def self.global_id_prefix=(value) + if StripeMock.client + StripeMock.client.set_server_global_id_prefix(value) + else + @global_id_prefix = value + end + end +end diff --git a/lib/stripe_mock/api/instance.rb b/lib/stripe_mock/api/instance.rb new file mode 100644 index 0000000..65ed2c7 --- /dev/null +++ b/lib/stripe_mock/api/instance.rb @@ -0,0 +1,38 @@ +module StripeMock + + @state = 'ready' + @instance = nil + @original_execute_request_method = Stripe::StripeClient.instance_method(:execute_request) + + def self.start + return false if @state == 'live' + @instance = instance = Instance.new + Stripe::StripeClient.send(:define_method, :execute_request) { |*args, **keyword_args| instance.mock_request(*args, **keyword_args) } + @state = 'local' + end + + def self.stop + return unless @state == 'local' + restore_stripe_execute_request_method + @instance = nil + @state = 'ready' + end + + # Yield the given block between StripeMock.start and StripeMock.stop + def self.mock(&block) + begin + self.start + yield + ensure + self.stop + end + end + + def self.restore_stripe_execute_request_method + Stripe::StripeClient.send(:define_method, :execute_request, @original_execute_request_method) + end + + def self.instance; @instance; end + def self.state; @state; end + +end diff --git a/lib/stripe_mock/api/live.rb b/lib/stripe_mock/api/live.rb new file mode 100644 index 0000000..a913b97 --- /dev/null +++ b/lib/stripe_mock/api/live.rb @@ -0,0 +1,15 @@ +module StripeMock + + def self.toggle_live(toggle) + if @state != 'ready' && @state != 'live' + raise "You cannot toggle StripeMock live when it has already started." + end + if toggle + @state = 'live' + StripeMock.set_default_test_helper_strategy(:live) + else + @state = 'ready' + StripeMock.set_default_test_helper_strategy(:mock) + end + end +end diff --git a/lib/stripe_mock/api/server.rb b/lib/stripe_mock/api/server.rb new file mode 100644 index 0000000..5fbfdc9 --- /dev/null +++ b/lib/stripe_mock/api/server.rb @@ -0,0 +1,39 @@ +module StripeMock + @default_server_pid_path = './stripe-mock-server.pid' + @default_server_log_path = './stripe-mock-server.log' + + class << self + attr_writer :default_server_pid_path, :default_server_log_path + + ["pid", "log"].each do |config_type| + define_method("default_server_#{config_type}_path") do + instance_variable_get("@default_server_#{config_type}_path") || "./stripe-mock-server.#{config_type}" + end + end + + def spawn_server(opts={}) + pid_path = opts[:pid_path] || default_server_pid_path + log_path = opts[:log_path] || default_server_log_path + + Dante::Runner.new('stripe-mock-server').execute( + :daemonize => true, :pid_path => pid_path, :log_path => log_path + ){ + StripeMock::Server.start_new(opts) + } + at_exit { + begin + e = $! # last exception + kill_server(pid_path) + ensure + raise e if $! != e + end + } + end + + def kill_server(pid_path=nil) + puts "Killing server at #{pid_path}" + path = pid_path || default_server_pid_path + Dante::Runner.new('stripe-mock-server').execute(:kill => true, :pid_path => path) + end + end +end diff --git a/lib/stripe_mock/api/test_helpers.rb b/lib/stripe_mock/api/test_helpers.rb new file mode 100644 index 0000000..5720065 --- /dev/null +++ b/lib/stripe_mock/api/test_helpers.rb @@ -0,0 +1,24 @@ +module StripeMock + + def self.create_test_helper(strategy=nil) + if strategy + get_test_helper_strategy(strategy).new + elsif @__test_strat + @__test_strat.new + else + TestStrategies::Mock.new + end + end + + def self.set_default_test_helper_strategy(strategy) + @__test_strat = get_test_helper_strategy(strategy) + end + + def self.get_test_helper_strategy(strategy) + case strategy.to_sym + when :mock then TestStrategies::Mock + when :live then TestStrategies::Live + else raise "Invalid test helper strategy: #{strategy.inspect}" + end + end +end diff --git a/lib/stripe_mock/api/webhooks.rb b/lib/stripe_mock/api/webhooks.rb new file mode 100644 index 0000000..0b92a4b --- /dev/null +++ b/lib/stripe_mock/api/webhooks.rb @@ -0,0 +1,99 @@ +module StripeMock + + def self.mock_webhook_payload(type, params = {}) + + fixture_file = File.join(@webhook_fixture_path, "#{type}.json") + + unless File.exist?(fixture_file) + unless Webhooks.event_list.include?(type) + raise UnsupportedRequestError.new "Unsupported webhook event `#{type}` (Searched in #{@webhook_fixture_path})" + end + fixture_file = File.join(@webhook_fixture_fallback_path, "#{type}.json") + end + + json = MultiJson.load File.read(fixture_file) + + json = Stripe::Util.symbolize_names(json) + params = Stripe::Util.symbolize_names(params) + json[:account] = params.delete(:account) if params.key?(:account) + json[:data][:object] = Util.rmerge(json[:data][:object], params) + json.delete(:id) + json[:created] = params[:created] || Time.now.to_i + + if @state == 'local' + event_data = instance.generate_webhook_event(json) + elsif @state == 'remote' + event_data = client.generate_webhook_event(json) + else + raise UnstartedStateError + end + event_data + end + + def self.mock_webhook_event(type, params={}) + Stripe::Event.construct_from(mock_webhook_payload(type, params)) + end + + module Webhooks + def self.event_list + @__list = [ + 'account.updated', + 'account.application.deauthorized', + 'account.external_account.created', + 'account.external_account.updated', + 'account.external_account.deleted', + 'balance.available', + 'charge.succeeded', + 'charge.updated', + 'charge.failed', + 'charge.refunded', + 'charge.dispute.created', + 'charge.dispute.updated', + 'charge.dispute.closed', + 'charge.dispute.funds_reinstated', + 'charge.dispute.funds_withdrawn', + 'checkout.session.completed.payment_mode', + 'checkout.session.completed.setup_mode', + 'customer.source.created', + 'customer.source.deleted', + 'customer.source.updated', + 'customer.created', + 'customer.updated', + 'customer.deleted', + 'customer.subscription.created', + 'customer.subscription.updated', + 'customer.subscription.deleted', + 'customer.subscription.trial_will_end', + 'customer.discount.created', + 'customer.discount.updated', + 'customer.discount.deleted', + 'invoice.created', + 'invoice.updated', + 'invoice.payment_succeeded', + 'invoice.payment_failed', + 'invoice.upcoming', + 'invoiceitem.created', + 'invoiceitem.updated', + 'invoiceitem.deleted', + 'mandate.updated', + 'payment_intent.canceled', + 'payment_intent.processing', + 'payment_intent.succeeded', + 'payment_intent.payment_failed', + 'plan.created', + 'plan.updated', + 'plan.deleted', + 'product.created', + 'product.updated', + 'product.deleted', + 'coupon.created', + 'coupon.deleted', + 'transfer.created', + 'transfer.paid', + 'transfer.updated', + 'transfer.failed' + ] + end + end + +end diff --git a/lib/stripe_mock/client.rb b/lib/stripe_mock/client.rb new file mode 100644 index 0000000..615fcdd --- /dev/null +++ b/lib/stripe_mock/client.rb @@ -0,0 +1,128 @@ +module StripeMock + class Client + attr_reader :port, :state + + def initialize(port) + @port = port + + DRb.start_service + @pipe = DRbObject.new_with_uri "druby://localhost:#{port}" + + # Ensure client can connect to server + timeout_wrap(5) { @pipe.ping } + @state = 'ready' + end + + def mock_request(method, url, api_key: nil, params: {}, headers: {}) + timeout_wrap do + @pipe.mock_request(method, url, api_key: api_key, params: params, headers: headers).tap {|result| + response, api_key = result + if response.is_a?(Hash) && response[:error_raised] == 'invalid_request' + args, keyword_args = response[:error_params].first(2), response[:error_params].last + raise Stripe::InvalidRequestError.new(*args, **keyword_args) + end + } + end + end + + def get_server_data(key) + timeout_wrap { + # Massage the data make this behave the same as the local StripeMock.start + result = {} + @pipe.get_data(key).each {|k,v| result[k] = Stripe::Util.symbolize_names(v) } + result + } + end + + def error_queue + timeout_wrap { @pipe.error_queue } + end + + def set_server_debug(toggle) + timeout_wrap { @pipe.set_debug(toggle) } + end + + def server_debug? + timeout_wrap { @pipe.debug? } + end + + def set_server_global_id_prefix(value) + timeout_wrap { @pipe.set_global_id_prefix(value) } + end + + def server_global_id_prefix + timeout_wrap { @pipe.global_id_prefix } + end + + def generate_bank_token(recipient_params) + timeout_wrap { @pipe.generate_bank_token(recipient_params) } + end + + def generate_card_token(card_params) + timeout_wrap { @pipe.generate_card_token(card_params) } + end + + def generate_webhook_event(event_data) + timeout_wrap { Stripe::Util.symbolize_names @pipe.generate_webhook_event(event_data) } + end + + def get_conversion_rate + timeout_wrap { @pipe.get_data(:conversion_rate) } + end + + def set_conversion_rate(value) + timeout_wrap { @pipe.set_conversion_rate(value) } + end + + def set_account_balance(value) + timeout_wrap { @pipe.set_account_balance(value) } + end + + def destroy_resource(type, id) + timeout_wrap { @pipe.destroy_resource(type, id) } + end + + def clear_server_data + timeout_wrap { @pipe.clear_data } + end + + def upsert_stripe_object(object, attributes) + timeout_wrap { @pipe.upsert_stripe_object(object, attributes) } + end + + def close! + self.cleanup + StripeMock.stop_client(:clear_server_data => false) + end + + def cleanup + return if @state == 'closed' + set_server_debug(false) + @state = 'closed' + end + + def timeout_wrap(tries=1) + original_tries = tries + begin + raise ClosedClientConnectionError if @state == 'closed' + yield + rescue ClosedClientConnectionError + raise + rescue Errno::ECONNREFUSED, DRb::DRbConnError => e + tries -= 1 + if tries > 0 + if tries == original_tries - 1 + print "Waiting for StripeMock Server.." + else + print '.' + end + sleep 1 + retry + else + raise StripeMock::ServerTimeoutError.new(e) + end + end + end + end + +end diff --git a/lib/stripe_mock/data.rb b/lib/stripe_mock/data.rb new file mode 100644 index 0000000..f3153f9 --- /dev/null +++ b/lib/stripe_mock/data.rb @@ -0,0 +1,1388 @@ +module StripeMock + module Data + + def self.mock_account(params = {}) + id = params[:id] || 'acct_103ED82ePvKYlo2C' + currency = params[:currency] || StripeMock.default_currency + { + id: id, + email: "bob@example.com", + statement_descriptor: nil, + display_name: "Stripe.com", + timezone: "US/Pacific", + details_submitted: false, + charges_enabled: false, + payouts_enabled: false, + currencies_supported: [ + "usd" + ], + default_currency: currency, + country: "US", + object: "account", + business_name: "Stripe.com", + business_url: nil, + support_phone: nil, + managed: false, + product_description: nil, + debit_negative_balances: true, + bank_accounts: { + object: "list", + total_count: 0, + has_more: false, + url: "/v1/accounts/#{id}/bank_accounts", + data: [ + + ] + }, + verification: { + fields_needed: [], + due_by: nil, + contacted: false + }, + transfer_schedule: { + delay_days: 7, + interval: "daily" + }, + tos_acceptance: { + ip: nil, + date: nil, + user_agent: nil + }, + external_accounts: { + object: "list", + data: [ + + ], + has_more: false, + total_count: 0, + url: "/v1/accounts/#{id}/external_accounts" + }, + legal_entity: { + type: nil, + business_name: nil, + address: { + line1: nil, + line2: nil, + city: nil, + state: nil, + postal_code: nil, + country: "US" + }, + first_name: nil, + last_name: nil, + personal_address: { + line1: nil, + line2: nil, + city: nil, + state: nil, + postal_code: nil, + country: nil + }, + dob: { + day: nil, + month: nil, + year: nil + }, + additional_owners: nil, + verification: { + status: "unverified", + document: nil, + details: nil + } + }, + decline_charge_on: { + cvc_failure: false, + avs_failure: false + }, + keys: { + secret: "sk_test_AmJhMTLPtY9JL4c6EG0", + publishable: "pk_test_2rSaMTLPtY9JL449dsf" + } + }.merge(params) + end + + def self.mock_account_link(params = {}) + now = Time.now.to_i + { + object: 'account_link', + created: now, + expires_at: now + 300, + url: 'https://connect.stripe.com/setup/c/iB0ph1cPnRLY', + data: {} + }.merge(params) + end + + def self.mock_express_login_link(params = {}) + now = Time.now.to_i + { + object: 'login_link', + created: now, + url: 'https://connect.stripe.com/express/Ln7FfnNpUcCU', + data: {} + }.merge(params) + end + + def self.mock_tax_rate(params) + { + id: 'test_cus_default', + object: 'tax_rate', + active: true, + created: 1559079603, + description: nil, + display_name: 'VAT', + inclusive: false, + jurisdiction: 'EU', + livemode: false, + metadata: {}, + percentage: 21.0 + }.merge(params) + end + + def self.mock_customer(sources, params) + cus_id = params[:id] || "test_cus_default" + currency = params[:currency] + sources.each {|source| source[:customer] = cus_id} + { + email: 'stripe_mock@example.com', + description: 'an auto-generated stripe customer data mock', + object: "customer", + created: 1372126710, + id: cus_id, + name: nil, + preferred_locales: [], + livemode: false, + delinquent: false, + discount: nil, + account_balance: 0, + currency: currency, + invoice_settings: { + default_payment_method: nil, + custom_fields: nil, + footer: nil + }, + sources: { + object: "list", + total_count: sources.size, + has_more: false, + url: "/v1/customers/#{cus_id}/sources", + data: sources + }, + subscriptions: { + object: "list", + total_count: 0, + has_more: false, + url: "/v1/customers/#{cus_id}/subscriptions", + data: [] + }, + default_source: nil + }.merge(params) + end + + def self.mock_charge(params={}) + charge_id = params[:id] || "ch_1fD6uiR9FAA2zc" + currency = params[:currency] || StripeMock.default_currency + { + id: charge_id, + object: "charge", + created: 1366194027, + livemode: false, + paid: true, + amount: 0, + application_fee: nil, + application_fee_amount: nil, + currency: currency, + destination: nil, + fraud_details: {}, + payment_method_details: { + card: { + brand: "visa", + checks: { + address_line1_check: nil, + address_postal_code_check: nil, + cvc_check: "pass" + }, + country: "US", + exp_month: 12, + exp_year: 2013, + fingerprint: "3TQGpK9JoY1GgXPw", + funding: "credit", + installments: nil, + last4: "4242", + network: "visa", + three_d_secure: nil, + wallet: nil + }, + type: "card" + }, + receipt_email: nil, + receipt_number: nil, + refunded: false, + shipping: {}, + statement_descriptor: "Charge #{charge_id}", + status: 'succeeded', + source: { + object: "card", + last4: "4242", + type: "Visa", + brand: "Visa", + funding: "credit", + exp_month: 12, + exp_year: 2013, + fingerprint: "3TQGpK9JoY1GgXPw", + country: "US", + name: "name", + address_line1: nil, + address_line2: nil, + address_city: nil, + address_state: nil, + address_zip: nil, + address_country: nil, + cvc_check: nil, + address_line1_check: nil, + address_zip_check: nil + }, + captured: params.has_key?(:capture) ? params.delete(:capture) : true, + refunds: { + object: "list", + total_count: 0, + has_more: false, + url: "/v1/charges/#{charge_id}/refunds", + data: [] + }, + transfer: nil, + balance_transaction: params[:balance_transaction] || "txn_2dyYXXP90MN26R", + failure_message: nil, + failure_code: nil, + amount_refunded: 0, + customer: nil, + invoice: nil, + description: nil, + dispute: nil, + metadata: { + } + }.merge(params) + end + + def self.mock_refund(params={}) + currency = params[:currency] || StripeMock.default_currency + { + id: "re_4fWhgUh5si7InF", + amount: 1, + currency: currency, + created: 1409165988, + object: "refund", + balance_transaction: "txn_4fWh2RKvgxcXqV", + metadata: {}, + charge: "ch_4fWhYjzQ23UFWT", + receipt_number: nil, + status: "succeeded", + reason: "requested_by_customer" + }.merge(params) + end + + def self.mock_charge_array + { + :data => [test_charge, test_charge, test_charge], + :object => 'list', + :url => '/v1/charges' + } + end + + def self.mock_card(params={}) + StripeMock::Util.card_merge({ + id: "test_cc_default", + object: "card", + last4: "4242", + type: "Visa", + brand: "Visa", + funding: "credit", + exp_month: 4, + exp_year: 2016, + fingerprint: "wXWJT135mEK107G8", + customer: "test_cus_default", + country: "US", + name: "Johnny App", + address_line1: nil, + address_line2: nil, + address_city: nil, + address_state: nil, + address_zip: nil, + address_country: nil, + cvc_check: nil, + address_line1_check: nil, + address_zip_check: nil, + tokenization_method: nil, + metadata: {} + }, params) + end + + def self.mock_bank_account(params={}) + currency = params[:currency] || StripeMock.default_currency + { + id: "test_ba_default", + object: "bank_account", + bank_name: "STRIPEMOCK TEST BANK", + last4: "6789", + routing_number: '110000000', + country: "US", + currency: currency, + validated: false, + status: 'new', + account_holder_name: 'John Doe', + account_holder_type: 'individual', + fingerprint: "aBcFinGerPrINt123", + metadata: {} + }.merge(params) + end + + def self.mock_coupon(params={}) + { + :duration_in_months => 3, + :percent_off => 25, + :amount_off => nil, + :currency => nil, + :id => "co_test_coupon", + :object => "coupon", + :max_redemptions => nil, + :redeem_by => nil, + :times_redeemed => 0, + :valid => true, + :metadata => {}, + }.merge(params) + end + + #FIXME nested overrides would be better than hardcoding plan_id + def self.mock_subscription(params={}) + StripeMock::Util.rmerge({ + created: 1478204116, + billing: 'charge_automatically', + current_period_start: 1308595038, + current_period_end: 1308681468, + status: 'trialing', + trial_from_plan: false, + plan: { + interval: 'month', + amount: 7500, + trial_period_days: 30, + object: 'plan', + id: '__test_plan_id__' + }, + items: { + object: 'list', + data: [{ + id: 'si_1AwFf62eZvKYlo2C9u6Dhf9', + created: 1504035973, + metadata: {}, + object: 'subscription_item', + plan: { + amount: 999, + created: 1504035972, + currency: StripeMock.default_currency + }, + quantity: 1 + }] + }, + cancel_at_period_end: false, + canceled_at: nil, + collection_method: 'charge_automatically', + ended_at: nil, + start_date: 1308595038, + object: 'subscription', + trial_start: 1308595038, + trial_end: 1308681468, + customer: 'c_test_customer', + quantity: 1, + tax_percent: nil, + discount: nil, + metadata: {}, + default_tax_rates: nil, + default_payment_method: nil, + pending_invoice_item_interval: nil, + next_pending_invoice_item_invoice: nil, + latest_invoice: nil + }, params) + end + + def self.mock_invoice(lines, params={}) + in_id = params[:id] || "test_in_default" + currency = params[:currency] || StripeMock.default_currency + lines << Data.mock_line_item() if lines.empty? + invoice = { + id: 'in_test_invoice', + status: 'open', + invoice_pdf: 'pdf_url', + hosted_invoice_url: 'hosted_invoice_url', + created: 1349738950, + period_end: 1349738950, + period_start: 1349738950, + due_date: nil, + lines: { + object: "list", + total_count: lines.count, + has_more: false, + url: "/v1/invoices/#{in_id}/lines", + data: lines + }, + subtotal: lines.map {|line| line[:amount]}.reduce(0, :+), + customer: "test_customer", + object: 'invoice', + attempted: false, + application_fee: nil, + closed: false, + description: nil, + forgiven: false, + metadata: {}, + paid: false, + receipt_number: nil, + statement_descriptor: nil, + tax: 10, + tax_percent: nil, + webhooks_delivered_at: 1349825350, + livemode: false, + attempt_count: 0, + amount_due: 100, + amount_paid: 0, + currency: currency, + starting_balance: 0, + ending_balance: 0, + next_payment_attempt: 1349825350, + charge: nil, + discount: nil, + subscription: nil + }.merge(params) + if invoice[:discount] + invoice[:total] = [0, invoice[:subtotal] - invoice[:discount][:coupon][:amount_off]].max if invoice[:discount][:coupon][:amount_off] + invoice[:total] = invoice[:subtotal] * invoice[:discount][:coupon][:percent_off] / 100 if invoice[:discount][:coupon][:percent_off] + else + invoice[:total] = invoice[:subtotal] + end + due = invoice[:total] + invoice[:starting_balance] + invoice[:amount_due] = due < 0 ? 0 : due + invoice[:ending_balance] = invoice[:starting_balance] + invoice[:total] if invoice[:amount_due] == 0 + invoice + end + + def self.mock_line_item(params = {}) + currency = params[:currency] || StripeMock.default_currency + { + id: "ii_test", + object: "line_item", + type: "invoiceitem", + livemode: false, + amount: 1000, + currency: currency, + discountable: false, + proration: false, + period: { + start: 1349738920, + end: 1349738920 + }, + tax_amounts: [ + { + amount: 10 + } + ], + quantity: nil, + subscription: nil, + plan: nil, + description: "Test invoice item", + metadata: {} + }.merge(params) + end + + def self.mock_invoice_item(params = {}) + currency = params[:currency] || StripeMock.default_currency + { + id: "test_ii", + object: "invoiceitem", + created: 1349738920, + amount: 1099, + livemode: false, + proration: false, + currency: currency, + customer: "cus_test", + description: "invoice item desc", + metadata: {}, + invoice: nil, + subscription: nil + }.merge(params) + end + + def self.mock_paid_invoice + test_invoice.merge({ + :attempt_count => 1, + :attempted => true, + :closed => true, + :paid => true, + :charge => 'ch_test_charge', + :ending_balance => 0, + :next_payment_attempt => nil, + }) + end + + def self.mock_invoice_customer_array + { + :data => [test_invoice], + :object => 'list', + :url => '/v1/invoices?customer=test_customer' + } + end + + def self.mock_order(order_items, params) + or_id = params[:id] || "test_or_default" + currency = params[:currency] || 'eur' + order_items << Data.mock_order_item if order_items.empty? + { + id: or_id, + object: "order", + amount: 5000, + application: nil, + application_fee: nil, + charge: nil, + created: 1448272783, + currency: currency, + customer: nil, + email: nil, + items: order_items, + livemode: false, + metadata: {}, + selected_shipping_method: nil, + shipping: { + address: { + city: "Anytown", + country: "US", + line1: "1234 Main street", + line2: nil, + postal_code: "123456", + state: nil + }, + name: "Jenny Rosen", + phone: nil + }, + shipping_methods: nil, + status: "created", + updated: 1448272783 + }.merge(params) + end + + def self.mock_order_item(params={}) + currency = params[:currency] || 'eur' + { + object: "order_item", + amount: 5000, + currency: currency, + description: "Anyitem", + parent: "sku_parent", + quantity: 1, + type: "sku" + }.merge(params) + end + + def self.mock_plan(params={}) + currency = params[:currency] || StripeMock.default_currency + { + id: "mock_plan_123", + object: "plan", + active: true, + aggregate_usage: nil, + amount: 2300, + billing_scheme: "per_unit", + created: 1466698898, + currency: currency, + interval: "month", + interval_count: 1, + livemode: false, + metadata: {}, + nickname: "My Mock Plan", + product: "mock_prod_NONEXIST", # override this with your own existing product id + tiers: nil, + tiers_mode: nil, + transform_usage: nil, + trial_period_days: nil, + usage_type: "licensed" + }.merge(params) + end + + def self.mock_price(params={}) + currency = params[:currency] || StripeMock.default_currency + { + id: "mock_price_123", + object: "price", + active: true, + billing_scheme: "per_unit", + created: 1593044959, + currency: currency, + livemode: false, + lookup_key: nil, + metadata: {}, + nickname: 'My Mock Price', + product: "mock_prod_NONEXIST", # override this with your own existing product id + recurring: { + aggregate_usage: nil, + interval: "month", + interval_count: 1, + usage_type: "licensed" + }, + tiers_mode: nil, + transform_quantity: nil, + type: "recurring", + unit_amount: 2000, + unit_amount_decimal: "2000" + }.merge(params) + end + + def self.mock_product(params={}) + { + id: "mock_prod_abc123", + object: "product", + active: true, + attributes:[], + caption: nil, + created: 1466698000, + deactivate_on: [], + description: nil, + images: [], + livemode: false, + metadata: {}, + name: "The Mock Product", + package_dimensions: nil, + shippable: nil, + statement_descriptor: nil, + type: "service", + unit_label: "my_unit", + updated: 1537939442, + url: nil + }.merge(params) + end + + def self.mock_recipient(cards, params={}) + rp_id = params[:id] || "test_rp_default" + cards.each {|card| card[:recipient] = rp_id} + { + name: "Stripe User", + type: "individual", + livemode: false, + object: "recipient", + id: rp_id, + active_account: { + last4: "6789", + bank_name: "STRIPE TEST BANK", + country: "US", + object: "bank_account" + }, + created: 1304114758, + verified: true, + metadata: { + }, + cards: { + object: "list", + url: "/v1/recipients/#{rp_id}/cards", + data: cards, + has_more: false, + total_count: cards.count + }, + default_card: nil + }.merge(params) + end + + def self.mock_recipient_array + { + :data => [test_recipient, test_recipient, test_recipient], + :object => 'list', + :url => '/v1/recipients' + } + end + + def self.mock_card_token(params={}) + { + :id => 'tok_default', + :livemode => false, + :used => false, + :object => 'token', + :type => 'card', + :card => { + :id => 'card_default', + :object => 'card', + :last4 => '2222', + :type => 'Visa', + :brand => 'Visa', + :funding => 'credit', + :exp_month => 9, + :exp_year => 2017, + :fingerprint => 'JRRLXGh38NiYygM7', + :customer => nil, + :country => 'US', + :name => nil, + :address_line1 => nil, + :address_line2 => nil, + :address_city => nil, + :address_state => nil, + :address_zip => nil, + :address_country => nil + } + }.merge(params) + end + + def self.mock_bank_account_token(params={}) + { + :id => 'tok_default', + :livemode => false, + :used => false, + :object => 'token', + :type => 'bank_account', + :bank_account => { + :id => 'bank_account_default', + :object => 'bank_account', + :last4 => '2222', + :fingerprint => 'JRRLXGh38NiYygM7', + } + }.merge(params) + end + + def self.mock_transfer(params={}) + currency = params[:currency] || StripeMock.default_currency + id = params[:id] || 'tr_test_transfer' + { + :amount => 100, + :amount_reversed => 0, + :balance_transaction => "txn_2dyYXXP90MN26R", + :id => id, + :livemode => false, + :metadata => {}, + :currency => currency, + :object => "transfer", + :created => 1304114826, + :description => "Transfer description", + :reversed => false, + :reversals => { + :object => "list", + :data => [], + :total_count => 0, + :has_more => false, + :url => "/v1/transfers/#{id}/reversals" + }, + :destination => "acct_164wxjKbnvuxQXGu", + :destination_payment => "py_164xRvKbnvuxQXGuVFV2pZo1", + :source_transaction => "ch_164xRv2eZvKYlo2Clu1sIJWB", + :source_type => "card", + :transfer_group => "group_ch_164xRv2eZvKYlo2Clu1sIJWB", + }.merge(params) + end + + def self.mock_payout(params={}) + currency = params[:currency] || StripeMock.default_currency + id = params[:id] || 'po_test_payout' + { + :amount => 100, + :id => id, + :livemode => false, + :metadata => {}, + :currency => currency, + :object => "payout", + :date => 1304114826, + :description => "Payout description", + }.merge(params) + end + + def self.mock_disputes(ids=[]) + disputes = {} + ids.each do |id| + disputes[id] = self.mock_dispute(id: id) + end + disputes + end + + def self.mock_dispute(params={}) + @timestamp ||= Time.now.to_i + currency = params[:currency] || StripeMock.default_currency + id = params[:id] || "dp_test_dispute" + { + :id => id, + :object => "dispute", + :amount => 195, + :balance_transactions => [], + :charge => "ch_15RsQR2eZvKYlo2CA8IfzCX0", + :created => @timestamp += 1, + :currency => currency, + :evidence => self.mock_dispute_evidence, + :evidence_details => self.mock_dispute_evidence_details, + :is_charge_refundable => false, + :livemode => false, + :metadata => {}, + :reason => "general", + :status => "under_review" + }.merge(params) + end + + def self.mock_dispute_evidence + { + :access_activity_log => nil, + :billing_address => nil, + :cancellation_policy => nil, + :cancellation_policy_disclosure => nil, + :cancellation_rebuttal => nil, + :customer_communication => nil, + :customer_email_address => nil, + :customer_name => nil, + :customer_purchase_ip => nil, + :customer_signature => nil, + :duplicate_charge_documentation => nil, + :duplicate_charge_explanation => nil, + :duplicate_charge_id => nil, + :product_description => nil, + :receipt => nil, + :refund_policy => nil, + :refund_policy_disclosure => nil, + :refund_refusal_explanation => nil, + :service_date => nil, + :service_documentation => nil, + :shipping_address => nil, + :shipping_carrier => nil, + :shipping_date => nil, + :shipping_documentation => nil, + :shipping_tracking_number => nil, + :uncategorized_file => nil, + :uncategorized_text => nil + } + end + + def self.mock_dispute_evidence_details + { + :due_by => 1424303999, + :has_evidence => false, + :past_due => false, + :submission_count => 0 + } + end + + def self.mock_transfer_array + { + :data => [test_transfer, test_transfer, test_transfer], + :object => 'list', + :url => '/v1/transfers' + } + end + + def self.mock_invalid_api_key_error + { + "error" => { + "type" => "invalid_request_error", + "message" => "Invalid API Key provided: invalid" + } + } + end + + def self.mock_invalid_exp_year_error + { + "error" => { + "code" => "invalid_expiry_year", + "param" => "exp_year", + "type" => "card_error", + "message" => "Your card's expiration year is invalid" + } + } + end + + def self.mock_missing_id_error + { + :error => { + :param => "id", + :type => "invalid_request_error", + :message => "Missing id" + } + } + end + + def self.mock_delete_subscription(params={}) + { + deleted: true + }.merge(params) + end + + def self.mock_api_error + { + :error => { + :type => "api_error" + } + } + end + + def self.mock_delete_discount_response + { + :deleted => true, + :id => "di_test_coupon" + } + end + + def self.mock_list_object(data, params={}) + list = StripeMock::Data::List.new(data, params) + list.to_h + end + + def self.mock_country_spec(country_code) + id = country_code || "US" + { + "id"=> "US", + "object"=> "country_spec", + "default_currency"=> "usd", + "supported_bank_account_currencies"=> {"usd"=>["US"]}, + "supported_payment_currencies"=> [ + "usd", + "aed", + "afn", + "all", + "amd", + "ang", + "aoa", + "ars", + "aud", + "awg", + "azn", + "bam", + "bbd", + "bdt", + "bgn", + "bif", + "bmd", + "bnd", + "bob", + "brl", + "bsd", + "bwp", + "bzd", + "cad", + "cdf", + "chf", + "clp", + "cny", + "cop", + "crc", + "cve", + "czk", + "djf", + "dkk", + "dop", + "dzd", + "egp", + "etb", + "eur", + "fjd", + "fkp", + "gbp", + "gel", + "gip", + "gmd", + "gnf", + "gtq", + "gyd", + "hkd", + "hnl", + "hrk", + "htg", + "huf", + "idr", + "ils", + "inr", + "isk", + "jmd", + "jpy", + "kes", + "kgs", + "khr", + "kmf", + "krw", + "kyd", + "kzt", + "lak", + "lbp", + "lkr", + "lrd", + "lsl", + "ltl", + "mad", + "mdl", + "mga", + "mkd", + "mnt", + "mop", + "mro", + "mur", + "mvr", + "mwk", + "mxn", + "myr", + "mzn", + "nad", + "ngn", + "nio", + "nok", + "npr", + "nzd", + "pab", + "pen", + "pgk", + "php", + "pkr", + "pln", + "pyg", + "qar", + "ron", + "rsd", + "rub", + "rwf", + "sar", + "sbd", + "scr", + "sek", + "sgd", + "shp", + "sll", + "sos", + "srd", + "std", + "svc", + "szl", + "thb", + "tjs", + "top", + "try", + "ttd", + "twd", + "tzs", + "uah", + "ugx", + "uyu", + "uzs", + "vnd", + "vuv", + "wst", + "xaf", + "xcd", + "xof", + "xpf", + "yer", + "zar", + "zmw" + ], + "supported_payment_methods"=> [ + "alipay", + "card", + "stripe" + ], + "verification_fields"=> {"individual"=>{"minimum"=>["external_account","legal_entity.address.city","legal_entity.address.line1","legal_entity.address.postal_code","legal_entity.address.state","legal_entity.dob.day","legal_entity.dob.month","legal_entity.dob.year","legal_entity.first_name","legal_entity.last_name","legal_entity.personal_id_number","legal_entity.ssn_last_4","legal_entity.type","tos_acceptance.date","tos_acceptance.ip"],"additional"=>["legal_entity.personal_id_number","legal_entity.verification.document"]},"company"=>{"minimum"=>["external_account","legal_entity.address.city","legal_entity.address.line1","legal_entity.address.postal_code","legal_entity.address.state","legal_entity.business_name","legal_entity.business_tax_id","legal_entity.dob.day","legal_entity.dob.month","legal_entity.dob.year","legal_entity.first_name","legal_entity.last_name","legal_entity.ssn_last_4","legal_entity.type","tos_acceptance.date","tos_acceptance.ip"],"additional"=>["legal_entity.personal_id_number","legal_entity.verification.document"]}} + } + end + + def self.mock_balance(usd_balance = 10000) + { + object: "balance", + available: [ + { + currency: "usd", + amount: usd_balance, + source_types: { + card: 25907032203, + bank_account: 108476658, + bitcoin_receiver: 1545182 + } + }], + instant_available: [ + { + currency: "usd", + amount: usd_balance, + source_types: { + card: 25907032203, + bank_account: 108476658, + bitcoin_receiver: 1545182 + } + }], + connect_reserved: [ + { + currency: "usd", + amount: 4700 + }], + livemode: false, + pending: [ + { + currency: "usd", + amount: 22738833554, + source_types: { + card: 22738826610, + bank_account: 0, + bitcoin_receiver: 6944 + } + }] + } + end + + def self.mock_balance_transactions(ids=[]) + bts = {} + ids.each do |id| + bts[id] = self.mock_balance_transaction(id: id) + end + bts + end + + def self.mock_balance_transaction(params = {}) + currency = params[:currency] || StripeMock.default_currency + bt_id = params[:id] || 'test_txn_default' + source = params[:source] || 'ch_test_charge' + { + id: bt_id, + object: "balance_transaction", + amount: 10000, + available_on: 1462406400, + created: 1461880226, + currency: currency, + description: nil, + fee: 320, + fee_details: [ + { + amount: 320, + application: nil, + currency: currency, + description: "Stripe processing fees", + type: "stripe_fee" + } + ], + net: 9680, + source: source, + sourced_transfers: { + object: "list", + data: [], + has_more: false, + total_count: 0, + url: "/v1/transfers?source_transaction=#{source}" + }, + status: "pending", + type: "charge" + }.merge(params) + end + + def self.mock_subscription_item(params = {}) + id = params[:id] || 'test_si_default' + { + id: id, + object: 'subscription_item', + created: 1504716183, + metadata: {}, + plan: { + id: 'PER_USER_PLAN1', + object: 'plan', + amount: 1337, + created: 1504716177, + currency: StripeMock.default_currency, + interval: 'month', + interval_count: 1, + livemode: false, + metadata: {}, + name: 'StripeMock Default Plan ID', + statement_descriptor: nil, + trial_period_days: nil + }, + quantity: 2 + }.merge(params) + end + + def self.mock_ephemeral_key(**params) + created = Time.now.to_i + expires = created + 34_000 + { + id: "ephkey_default", + object: "ephemeral_key", + associated_objects: [ + { + id: params[:customer], + type: "customer" + } + ], + created: created, + expires: expires, + livemode: false, + secret: "ek_test_default" + } + end + + def self.mock_payment_intent(params = {}) + payment_intent_id = params[:id] || "pi_1EwXFB2eZvKYlo2CggNnFBo8" + amount = params[:amount] || 49900 + currency = params[:currency] || StripeMock.default_currency + { + id: payment_intent_id, + object: "payment_intent", + amount: amount, + amount_capturable: 0, + amount_received: 0, + application: nil, + application_fee_amount: nil, + canceled_at: nil, + cancellation_reason: nil, + capture_method: "automatic", + charges: { + object: "list", + data: [], + has_more: false, + total_count: 1, + url: "/v1/charges?payment_intent=pi_1EwXFB2eZvKYlo2CggNnFBo8" + }, + client_secret: "pi_1EwXFB2eZvKYlo2CggNnFBo8_secret_vOMkpqZu8ca7hxhfiO80tpT3v", + confirmation_method: "manual", + created: 1563208901, + currency: currency, + customer: nil, + description: nil, + invoice: nil, + last_payment_error: nil, + livemode: false, + metadata: {}, + next_action: { type: "use_stripe_sdk" }, + on_behalf_of: nil, + payment_method: nil, + payment_method_types: [ + "card" + ], + receipt_email: nil, + review: nil, + setup_future_usage: nil, + shipping: nil, + source: nil, + statement_descriptor: nil, + status: "requires_action", + transfer_data: nil, + transfer_group: nil + }.merge(params) + end + + def self.mock_payment_method(params = {}) + payment_method_id = params[:id] || 'pm_1ExEuFL2DI6wht39WNJgbybl' + + type = params[:type].to_sym + data = { + card: { + brand: case params.dig(:card, :number)&.to_s + when /^4/, nil + 'visa' + when /^5[1-5]/ + 'mastercard' + else + 'unknown' + end, + checks: { + address_line1_check: nil, + address_postal_code_check: nil, + cvc_check: 'pass' + }, + country: 'FR', + exp_month: params.dig(:card, :exp_month) || 2, + exp_year: params.dig(:card, :exp_year) || 2022, + fingerprint: 'Hr3Ly5z5IYxsokWA', + funding: 'credit', + generated_from: nil, + last4: params.dig(:card, :number)&.[](-4..) || '3155', + three_d_secure_usage: { supported: true }, + wallet: nil + }, + ideal: { + bank: 'ing', + bic: 'INGBNL2A', + iban_last4: '****', + verified_name: 'JENNY ROSEN' + }, + sepa_debit: { + bank_code: '37040044', + branch_code: '', + country: 'DE', + fingerprint: 'FD81kbVPe7M05BMj', + last4: params.dig(:sepa_debit, :iban)&.[](-4..) || '3000' + } + } + + { + id: payment_method_id, + object: 'payment_method', + type: params[:type], + billing_details: { + address: { + city: 'New Orleans', + country: 'US', + line1: 'Bourbon Street 23', + line2: nil, + postal_code: '10000', + state: nil + }, + email: 'foo@bar.com', + name: 'John Dolton', + phone: nil + }, + customer: params[:customer] || nil, + metadata: { + order_id: '123456789' + } + }.merge(params).merge(type => data[type]) + end + + def self.mock_setup_intent(params = {}) + setup_intent_id = params[:id] || "seti_1F96eK2aLAadsDqo0AVIyPmC" + { + :id => setup_intent_id, + :object => "setup_intent", + :application => nil, + :cancellation_reason => nil, + :client_secret => "seti_1F96eK2aLAadsDqo0AVIyPmC_secret_FePTYgOoPFxDOUL53fFMSoTAyiXsWAV", + :created => 1566204936, + :customer => nil, + :description => nil, + :last_setup_error => nil, + :livemode => false, + :metadata => {}, + :next_action => nil, + :on_behalf_of => nil, + :payment_method => nil, + :payment_method_options => { + card: {request_three_d_secure: "automatic"} + }, + :payment_method_types => ["card"], + :status => "requires_payment_method", + :usage => "off_session" + }.merge(params) + end + + def self.mock_checkout_session(params = {}) + cs_id = params[:id] || "test_cs_default" + currency = params[:currency] || StripeMock.default_currency + { + id: cs_id, + object: 'checkout.session', + billing_address_collection: nil, + cancel_url: 'https://example.com/cancel', + client_reference_id: nil, + customer: nil, + customer_email: nil, + display_items: [ + { + amount: 1500, + currency: currency, + custom: { + description: 'Comfortable cotton t-shirt', + images: nil, + name: 'T-shirt' + }, + quantity: 2, + type: 'custom' + } + ], + livemode: false, + locale: nil, + mode: nil, + payment_intent: mock_payment_intent[:id], + payment_method_types: [ + 'card' + ], + setup_intent: nil, + submit_type: nil, + subscription: nil, + success_url: 'https://example.com/success' + }.merge(params) + end + end +end diff --git a/lib/stripe_mock/data/list.rb b/lib/stripe_mock/data/list.rb new file mode 100644 index 0000000..a3f10e9 --- /dev/null +++ b/lib/stripe_mock/data/list.rb @@ -0,0 +1,106 @@ +module StripeMock + module Data + class List + attr_reader :data, :limit, :offset, :starting_after, :ending_before, :active + + def initialize(data, options = {}) + @data = Array(data.clone) + @limit = [[options[:limit] || 10, 100].min, 1].max # restrict @limit to 1..100 + @starting_after = options[:starting_after] + @ending_before = options[:ending_before] + @active = options[:active] + if contains_stripe_objects? + prune_deleted_data + sort_data + end + end + + def url + "/v1/#{object_types}" + end + + def to_hash + { object: "list", data: data_page, url: url, has_more: has_more? } + end + alias_method :to_h, :to_hash + + def has_more? + (offset + limit) < data.size + end + + def method_missing(method_name, *args, &block) + hash = to_hash + + if hash.keys.include?(method_name) + hash[method_name] + else + super + end + end + + def respond_to?(method_name, priv = false) + to_hash.keys.include?(method_name) || super + end + + private + + def offset + case + when starting_after + index = data.index { |datum| datum[:id] == starting_after } + (index || raise("No such object id: #{starting_after}")) + 1 + when ending_before + index = data.index { |datum| datum[:id] == ending_before } + (index || raise("No such object id: #{ending_before}")) - 1 + else + 0 + end + end + + def data_page + filtered_data[offset, limit] + end + + def filtered_data + filtered_data = data + filtered_data = filtered_data.select { |d| d[:active] == active } unless active.nil? + + filtered_data + end + + def object_types + if first_object = data[0] + "#{first_object.class.to_s.split('::')[-1].downcase}s" + end + end + + def contains_stripe_objects? + return false if data.empty? + + object = data.first + object.is_a?(Stripe::StripeObject) || ( + object.is_a?(Hash) && [:created, :deleted].any? { |k| object.key?(k) } + ) + end + + def prune_deleted_data + data.reject! do |object| + (object.is_a?(Hash) && object[:deleted]) || + (object.is_a?(Stripe::StripeObject) && object.deleted?) + end + end + + def sort_data + # Reverse must follow sort to preserve existing test dependencies. The + # alternative would be to simply reverse lhs and rhs in the comparison, + # however, being a stable sort this breaks the existing dependency when + # more than one record share the same `created` value. + @data = data.sort { |lhs, rhs| sort_val(lhs) <=> sort_val(rhs) }.reverse + end + + def sort_val(object) + object.is_a?(Stripe::StripeObject) ? object.created : object[:created] + end + end + end +end diff --git a/lib/stripe_mock/error_queue.rb b/lib/stripe_mock/error_queue.rb new file mode 100644 index 0000000..e57fa19 --- /dev/null +++ b/lib/stripe_mock/error_queue.rb @@ -0,0 +1,27 @@ +require 'drb/drb' + +module StripeMock + class ErrorQueue + include DRb::DRbUndumped + extend DRb::DRbUndumped + + def initialize + @queue = [] + end + + def queue(error, handler_names) + @queue << handler_names.map {|n| [n, error]} + end + + def error_for_handler_name(handler_name) + return nil if @queue.count == 0 + triggers = @queue.first + (triggers.assoc(:all) || triggers.assoc(handler_name) || [])[1] + end + + def dequeue + @queue.shift + end + + end +end diff --git a/lib/stripe_mock/errors/closed_client_connection_error.rb b/lib/stripe_mock/errors/closed_client_connection_error.rb new file mode 100644 index 0000000..a2e15c8 --- /dev/null +++ b/lib/stripe_mock/errors/closed_client_connection_error.rb @@ -0,0 +1,9 @@ +module StripeMock + class ClosedClientConnectionError < StripeMockError + + def initialize + super("This StripeMock client has already been closed.") + end + + end +end diff --git a/lib/stripe_mock/errors/server_timeout_error.rb b/lib/stripe_mock/errors/server_timeout_error.rb new file mode 100644 index 0000000..d527aaa --- /dev/null +++ b/lib/stripe_mock/errors/server_timeout_error.rb @@ -0,0 +1,12 @@ +module StripeMock + class ServerTimeoutError < StripeMockError + + attr_reader :associated_error + + def initialize(associated_error) + @associated_error = associated_error + super("Unable to connect to stripe mock server (did you forget to run `$ stripe-mock-server`?)") + end + + end +end diff --git a/lib/stripe_mock/errors/stripe_mock_error.rb b/lib/stripe_mock/errors/stripe_mock_error.rb new file mode 100644 index 0000000..25b6ced --- /dev/null +++ b/lib/stripe_mock/errors/stripe_mock_error.rb @@ -0,0 +1,15 @@ +module StripeMock + class StripeMockError < StandardError + + attr_reader :message + + def initialize(message) + @message = message + end + + def to_s + @message + end + + end +end diff --git a/lib/stripe_mock/errors/uninitialized_instance_error.rb b/lib/stripe_mock/errors/uninitialized_instance_error.rb new file mode 100644 index 0000000..87ba8aa --- /dev/null +++ b/lib/stripe_mock/errors/uninitialized_instance_error.rb @@ -0,0 +1,9 @@ +module StripeMock + class UninitializedInstanceError < StripeMockError + + def initialize + super("StripeMock instance is nil (did you forget to call `StripeMock.start`?)") + end + + end +end diff --git a/lib/stripe_mock/errors/unstarted_state_error.rb b/lib/stripe_mock/errors/unstarted_state_error.rb new file mode 100644 index 0000000..d0e28e1 --- /dev/null +++ b/lib/stripe_mock/errors/unstarted_state_error.rb @@ -0,0 +1,9 @@ +module StripeMock + class UnstartedStateError < StripeMockError + + def initialize + super("StripeMock has not been started. Please call StripeMock.start or StripeMock.start_client") + end + + end +end diff --git a/lib/stripe_mock/errors/unsupported_request_error.rb b/lib/stripe_mock/errors/unsupported_request_error.rb new file mode 100644 index 0000000..0115d53 --- /dev/null +++ b/lib/stripe_mock/errors/unsupported_request_error.rb @@ -0,0 +1,4 @@ +module StripeMock + class UnsupportedRequestError < StripeMockError + end +end diff --git a/lib/stripe_mock/instance.rb b/lib/stripe_mock/instance.rb new file mode 100644 index 0000000..b1ad87d --- /dev/null +++ b/lib/stripe_mock/instance.rb @@ -0,0 +1,248 @@ +module StripeMock + class Instance + + include StripeMock::RequestHandlers::Helpers + include StripeMock::RequestHandlers::ParamValidators + + DUMMY_API_KEY = (0...32).map { (65 + rand(26)).chr }.join.downcase + + # Handlers are ordered by priority + @@handlers = [] + + def self.add_handler(route, name) + @@handlers << { + :route => %r{^#{route}$}, + :name => name + } + end + + def self.handler_for_method_url(method_url) + @@handlers.find {|h| method_url =~ h[:route] } + end + + include StripeMock::RequestHandlers::PaymentIntents + include StripeMock::RequestHandlers::PaymentMethods + include StripeMock::RequestHandlers::SetupIntents + include StripeMock::RequestHandlers::ExternalAccounts + include StripeMock::RequestHandlers::AccountLinks + include StripeMock::RequestHandlers::ExpressLoginLinks + include StripeMock::RequestHandlers::Accounts + include StripeMock::RequestHandlers::Balance + include StripeMock::RequestHandlers::BalanceTransactions + include StripeMock::RequestHandlers::Charges + include StripeMock::RequestHandlers::Cards + include StripeMock::RequestHandlers::Sources + include StripeMock::RequestHandlers::Subscriptions # must be before Customers + include StripeMock::RequestHandlers::SubscriptionItems + include StripeMock::RequestHandlers::Customers + include StripeMock::RequestHandlers::Coupons + include StripeMock::RequestHandlers::Disputes + include StripeMock::RequestHandlers::Events + include StripeMock::RequestHandlers::Invoices + include StripeMock::RequestHandlers::InvoiceItems + include StripeMock::RequestHandlers::Orders + include StripeMock::RequestHandlers::Plans + include StripeMock::RequestHandlers::Prices + include StripeMock::RequestHandlers::Products + include StripeMock::RequestHandlers::Refunds + include StripeMock::RequestHandlers::Recipients + include StripeMock::RequestHandlers::Transfers + include StripeMock::RequestHandlers::Tokens + include StripeMock::RequestHandlers::CountrySpec + include StripeMock::RequestHandlers::Payouts + include StripeMock::RequestHandlers::EphemeralKey + include StripeMock::RequestHandlers::TaxRates + include StripeMock::RequestHandlers::Checkout + include StripeMock::RequestHandlers::Checkout::Session + + attr_reader :accounts, :balance, :balance_transactions, :bank_tokens, :charges, :coupons, :customers, + :disputes, :events, :invoices, :invoice_items, :orders, :payment_intents, :payment_methods, + :setup_intents, :plans, :prices, :recipients, :refunds, :transfers, :payouts, :subscriptions, :country_spec, + :subscriptions_items, :products, :tax_rates, :checkout_sessions, :checkout_session_line_items + + attr_accessor :error_queue, :debug, :conversion_rate, :account_balance + + def initialize + @accounts = {} + @balance = Data.mock_balance + @balance_transactions = Data.mock_balance_transactions(['txn_05RsQX2eZvKYlo2C0FRTGSSA','txn_15RsQX2eZvKYlo2C0ERTYUIA', 'txn_25RsQX2eZvKYlo2C0ZXCVBNM', 'txn_35RsQX2eZvKYlo2C0QAZXSWE', 'txn_45RsQX2eZvKYlo2C0EDCVFRT', 'txn_55RsQX2eZvKYlo2C0OIKLJUY', 'txn_65RsQX2eZvKYlo2C0ASDFGHJ', 'txn_75RsQX2eZvKYlo2C0EDCXSWQ', 'txn_85RsQX2eZvKYlo2C0UJMCDET', 'txn_95RsQX2eZvKYlo2C0EDFRYUI']) + @bank_tokens = {} + @card_tokens = {} + @customers = { Stripe.api_key => {} } + @charges = {} + @payment_intents = {} + @payment_methods = {} + @setup_intents = {} + @coupons = {} + @disputes = Data.mock_disputes(['dp_05RsQX2eZvKYlo2C0FRTGSSA','dp_15RsQX2eZvKYlo2C0ERTYUIA', 'dp_25RsQX2eZvKYlo2C0ZXCVBNM', 'dp_35RsQX2eZvKYlo2C0QAZXSWE', 'dp_45RsQX2eZvKYlo2C0EDCVFRT', 'dp_55RsQX2eZvKYlo2C0OIKLJUY', 'dp_65RsQX2eZvKYlo2C0ASDFGHJ', 'dp_75RsQX2eZvKYlo2C0EDCXSWQ', 'dp_85RsQX2eZvKYlo2C0UJMCDET', 'dp_95RsQX2eZvKYlo2C0EDFRYUI']) + @events = {} + @invoices = {} + @invoice_items = {} + @orders = {} + @payment_methods = {} + @plans = {} + @prices = {} + @products = {} + @recipients = {} + @refunds = {} + @transfers = {} + @payouts = {} + @subscriptions = {} + @subscriptions_items = {} + @country_spec = {} + @tax_rates = {} + @checkout_sessions = {} + @checkout_session_line_items = {} + + @debug = false + @error_queue = ErrorQueue.new + @id_counter = 0 + @balance_transaction_counter = 0 + @dispute_counter = 0 + @conversion_rate = 1.0 + @account_balance = 10000 + + # This is basically a cache for ParamValidators + @base_strategy = TestStrategies::Base.new + end + + def mock_request(method, url, api_key: nil, api_base: nil, params: {}, headers: {}) + return {} if method == :xtest + + api_key ||= (Stripe.api_key || DUMMY_API_KEY) + + # Ensure params hash has symbols as keys + params = Stripe::Util.symbolize_names(params) + + method_url = "#{method} #{url}" + + if handler = Instance.handler_for_method_url(method_url) + if @debug == true + puts "- - - - " * 8 + puts "[StripeMock req]::#{handler[:name]} #{method} #{url}" + puts " #{params}" + end + + if mock_error = @error_queue.error_for_handler_name(handler[:name]) + @error_queue.dequeue + raise mock_error + else + res = self.send(handler[:name], handler[:route], method_url, params, headers) + puts " [res] #{res}" if @debug == true + [to_faraday_hash(res), api_key] + end + else + puts "[StripeMock] Warning : Unrecognized endpoint + method : [#{method} #{url}]" + puts "[StripeMock] params: #{params}" unless params.empty? + [{}, api_key] + end + end + + def generate_webhook_event(event_data) + event_data[:id] ||= new_id 'evt' + @events[ event_data[:id] ] = symbolize_names(event_data) + end + + def upsert_stripe_object(object, attributes) + # Most Stripe entities can be created via the API. However, some entities are created when other Stripe entities are + # created - such as when BalanceTransactions are created when Charges are created. This method provides the ability + # to create these internal entities. + # It also provides the ability to modify existing Stripe entities. + id = attributes[:id] + if id.nil? || id == "" + # Insert new Stripe object + case object + when :balance_transaction + id = new_balance_transaction('txn', attributes) + when :dispute + id = new_dispute('dp', attributes) + else + raise UnsupportedRequestError.new "Unsupported stripe object `#{object}`" + end + else + # Update existing Stripe object + case object + when :balance_transaction + btxn = assert_existence :balance_transaction, id, @balance_transactions[id] + btxn.merge!(attributes) + when :dispute + dispute = assert_existence :dispute, id, @disputes[id] + dispute.merge!(attributes) + else + raise UnsupportedRequestError.new "Unsupported stripe object `#{object}`" + end + end + id + end + + private + + def assert_existence(type, id, obj, message=nil) + if obj.nil? + msg = message || "No such #{type}: #{id}" + raise Stripe::InvalidRequestError.new(msg, type.to_s, http_status: 404) + end + obj + end + + def new_id(prefix) + # Stripe ids must be strings + "#{StripeMock.global_id_prefix}#{prefix}_#{@id_counter += 1}" + end + + def new_balance_transaction(prefix, params = {}) + # balance transaction ids must be strings + id = "#{StripeMock.global_id_prefix}#{prefix}_#{@balance_transaction_counter += 1}" + amount = params[:amount] + unless amount.nil? + # Fee calculation + calculate_fees(params) unless params[:fee] + params[:net] = amount - params[:fee] + params[:amount] = amount * @conversion_rate + end + @balance_transactions[id] = Data.mock_balance_transaction(params.merge(id: id)) + id + end + + def new_dispute(prefix, params = {}) + id = "#{StripeMock.global_id_prefix}#{prefix}_#{@dispute_counter += 1}" + @disputes[id] = Data.mock_dispute(params.merge(id: id)) + id + end + + def symbolize_names(hash) + Stripe::Util.symbolize_names(hash) + end + + def to_faraday_hash(hash) + response = Struct.new(:data) + response.new(hash) + end + + def calculate_fees(params) + application_fee = params[:application_fee] || 0 + params[:fee] = processing_fee(params[:amount]) + application_fee + params[:fee_details] = [ + { + amount: processing_fee(params[:amount]), + application: nil, + currency: params[:currency] || StripeMock.default_currency, + description: "Stripe processing fees", + type: "stripe_fee" + } + ] + if application_fee + params[:fee_details] << { + amount: application_fee, + currency: params[:currency] || StripeMock.default_currency, + description: "Application fee", + type: "application_fee" + } + end + end + + def processing_fee(amount) + (30 + (amount.abs * 0.029).ceil) * (amount > 0 ? 1 : -1) + end + end +end diff --git a/lib/stripe_mock/request_handlers/account_links.rb b/lib/stripe_mock/request_handlers/account_links.rb new file mode 100644 index 0000000..5d99e7e --- /dev/null +++ b/lib/stripe_mock/request_handlers/account_links.rb @@ -0,0 +1,15 @@ +module StripeMock + module RequestHandlers + module AccountLinks + + def AccountLinks.included(klass) + klass.add_handler 'post /v1/account_links', :new_account_link + end + + def new_account_link(route, method_url, params, headers) + route =~ method_url + Data.mock_account_link(params) + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/accounts.rb b/lib/stripe_mock/request_handlers/accounts.rb new file mode 100644 index 0000000..bac6796 --- /dev/null +++ b/lib/stripe_mock/request_handlers/accounts.rb @@ -0,0 +1,86 @@ +module StripeMock + module RequestHandlers + module Accounts + VALID_START_YEAR = 2009 + + def Accounts.included(klass) + klass.add_handler 'post /v1/accounts', :new_account + klass.add_handler 'get /v1/account', :get_account + klass.add_handler 'get /v1/accounts/(.*)', :get_account + klass.add_handler 'post /v1/accounts/(.*)', :update_account + klass.add_handler 'get /v1/accounts', :list_accounts + klass.add_handler 'post /oauth/deauthorize',:deauthorize + end + + def new_account(route, method_url, params, headers) + params[:id] ||= new_id('acct') + route =~ method_url + accounts[params[:id]] ||= Data.mock_account(params) + end + + def get_account(route, method_url, params, headers) + route =~ method_url + init_account + id = $1 || accounts.keys[0] + assert_existence :account, id, accounts[id] + end + + def update_account(route, method_url, params, headers) + route =~ method_url + account = assert_existence :account, $1, accounts[$1] + account.merge!(params) + if blank_value?(params[:tos_acceptance], :date) + raise Stripe::InvalidRequestError.new("Invalid integer: ", "tos_acceptance[date]", http_status: 400) + elsif params[:tos_acceptance] && params[:tos_acceptance][:date] + validate_acceptance_date(params[:tos_acceptance][:date]) + end + account + end + + def list_accounts(route, method_url, params, headers) + init_account + Data.mock_list_object(accounts.values, params) + end + + def deauthorize(route, method_url, params, headers) + init_account + route =~ method_url + Stripe::StripeObject.construct_from(:stripe_user_id => params[:stripe_user_id]) + end + + private + + def init_account + if accounts == {} + acc = Data.mock_account + accounts[acc[:id]] = acc + end + end + + # Checks if setting a blank value + # + # returns true if the key is included in the hash + # and its value is empty or nil + def blank_value?(hash, key) + if hash.key?(key) + value = hash[key] + return true if value.nil? || "" == value + end + false + end + + def validate_acceptance_date(unix_date) + unix_now = Time.now.strftime("%s").to_i + formatted_date = Time.at(unix_date) + + return if formatted_date.year >= VALID_START_YEAR && unix_now >= unix_date + + raise Stripe::InvalidRequestError.new( + "ToS acceptance date is not valid. Dates are expected to be integers, measured in seconds, not in the future, and after 2009", + "tos_acceptance[date]", + http_status: 400 + ) + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/balance.rb b/lib/stripe_mock/request_handlers/balance.rb new file mode 100644 index 0000000..1220b87 --- /dev/null +++ b/lib/stripe_mock/request_handlers/balance.rb @@ -0,0 +1,17 @@ +module StripeMock + module RequestHandlers + module Balance + + def Balance.included(klass) + klass.add_handler 'get /v1/balance', :get_balance + end + + def get_balance(route, method_url, params, headers) + route =~ method_url + + return_balance = Data.mock_balance(account_balance) + return_balance + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/balance_transactions.rb b/lib/stripe_mock/request_handlers/balance_transactions.rb new file mode 100644 index 0000000..50d29a0 --- /dev/null +++ b/lib/stripe_mock/request_handlers/balance_transactions.rb @@ -0,0 +1,37 @@ +module StripeMock + module RequestHandlers + module BalanceTransactions + + def BalanceTransactions.included(klass) + klass.add_handler 'get /v1/balance_transactions/(.*)', :get_balance_transaction + klass.add_handler 'get /v1/balance_transactions', :list_balance_transactions + end + + def get_balance_transaction(route, method_url, params, headers) + route =~ method_url + assert_existence :balance_transaction, $1, hide_additional_attributes(balance_transactions[$1]) + end + + def list_balance_transactions(route, method_url, params, headers) + values = balance_transactions.values + if params.has_key?(:transfer) + # If transfer supplied as params, need to filter the btxns returned to only include those with the specified transfer id + values = values.select{|btxn| btxn[:transfer] == params[:transfer]} + end + Data.mock_list_object(values.map{|btxn| hide_additional_attributes(btxn)}, params) + end + + private + + def hide_additional_attributes(btxn) + # For automatic Stripe transfers, the transfer attribute on balance_transaction stores the transfer which + # included this balance_transaction. However, it is not exposed as a field returned on a balance_transaction. + # Therefore, need to not show this attribute if it exists. + if !btxn.nil? + btxn.reject{|k,v| k == :transfer } + end + end + + end + end +end diff --git a/lib/stripe_mock/request_handlers/cards.rb b/lib/stripe_mock/request_handlers/cards.rb new file mode 100644 index 0000000..7829e2e --- /dev/null +++ b/lib/stripe_mock/request_handlers/cards.rb @@ -0,0 +1,35 @@ +module StripeMock + module RequestHandlers + module Cards + + def Cards.included(klass) + klass.add_handler 'get /v1/recipients/(.*)/cards', :retrieve_recipient_cards + klass.add_handler 'get /v1/recipients/(.*)/cards/(.*)', :retrieve_recipient_card + klass.add_handler 'post /v1/recipients/(.*)/cards', :create_recipient_card + klass.add_handler 'delete /v1/recipients/(.*)/cards/(.*)', :delete_recipient_card + end + + def create_recipient_card(route, method_url, params, headers) + route =~ method_url + add_card_to(:recipient, $1, params, recipients) + end + + def retrieve_recipient_cards(route, method_url, params, headers) + route =~ method_url + retrieve_object_cards(:recipient, $1, recipients) + end + + def retrieve_recipient_card(route, method_url, params, headers) + route =~ method_url + recipient = assert_existence :recipient, $1, recipients[$1] + + assert_existence :card, $2, get_card(recipient, $2, "Recipient") + end + + def delete_recipient_card(route, method_url, params, headers) + route =~ method_url + delete_card_from(:recipient, $1, $2, recipients) + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/charges.rb b/lib/stripe_mock/request_handlers/charges.rb new file mode 100644 index 0000000..54c3050 --- /dev/null +++ b/lib/stripe_mock/request_handlers/charges.rb @@ -0,0 +1,184 @@ +module StripeMock + module RequestHandlers + module Charges + + def Charges.included(klass) + klass.add_handler 'post /v1/charges', :new_charge + klass.add_handler 'get /v1/charges', :get_charges + klass.add_handler 'get /v1/charges/(.*)', :get_charge + klass.add_handler 'post /v1/charges/(.*)/capture', :capture_charge + klass.add_handler 'post /v1/charges/(.*)/refund', :refund_charge + klass.add_handler 'post /v1/charges/(.*)/refunds', :refund_charge + klass.add_handler 'post /v1/charges/(.*)', :update_charge + end + + def new_charge(route, method_url, params, headers = {}) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + + if headers && headers[:idempotency_key] + params[:idempotency_key] = headers[:idempotency_key] + if charges.any? + original_charge = charges.values.find { |c| c[:idempotency_key] == headers[:idempotency_key]} + return charges[original_charge[:id]] if original_charge + end + end + + id = new_id('ch') + + if params[:source] + if params[:source].is_a?(String) + # if a customer is provided, the card parameter is assumed to be the actual + # card id, not a token. in this case we'll find the card in the customer + # object and return that. + if params[:customer] + params[:source] = get_card(customers[stripe_account][params[:customer]], params[:source]) + else + params[:source] = get_card_or_bank_by_token(params[:source]) + end + elsif params[:source][:id] + raise Stripe::InvalidRequestError.new("Invalid token id: #{params[:source]}", 'card', http_status: 400) + end + elsif params[:customer] + customer = customers[stripe_account][params[:customer]] + if customer && customer[:default_source] + params[:source] = get_card(customer, customer[:default_source]) + end + end + + ensure_required_params(params) + bal_trans_params = { amount: params[:amount], source: id, application_fee: params[:application_fee] } + + balance_transaction_id = new_balance_transaction('txn', bal_trans_params) + + charges[id] = Data.mock_charge( + params.merge :id => id, + :balance_transaction => balance_transaction_id) + + charge = charges[id].clone + if params[:expand] == ['balance_transaction'] + charge[:balance_transaction] = + balance_transactions[balance_transaction_id] + end + + charge + end + + def update_charge(route, method_url, params, headers) + route =~ method_url + id = $1 + + charge = assert_existence :charge, id, charges[id] + allowed = allowed_params(params) + disallowed = params.keys - allowed + if disallowed.count > 0 + raise Stripe::InvalidRequestError.new("Received unknown parameters: #{disallowed.join(', ')}" , '', http_status: 400) + end + + charges[id] = Util.rmerge(charge, params) + end + + def get_charges(route, method_url, params, headers) + params[:offset] ||= 0 + params[:limit] ||= 10 + + clone = charges.clone + + if params[:customer] + clone.delete_if { |k,v| v[:customer] != params[:customer] } + end + + Data.mock_list_object(clone.values, params) + end + + def get_charge(route, method_url, params, headers) + route =~ method_url + charge_id = $1 || params[:charge] + charge = assert_existence :charge, charge_id, charges[charge_id] + + charge = charge.clone + if params[:expand] == ['balance_transaction'] + balance_transaction = balance_transactions[charge[:balance_transaction]] + charge[:balance_transaction] = balance_transaction + end + + charge + end + + def capture_charge(route, method_url, params, headers) + route =~ method_url + charge = assert_existence :charge, $1, charges[$1] + + if params[:amount] + refund = Data.mock_refund( + :balance_transaction => new_balance_transaction('txn'), + :id => new_id('re'), + :amount => charge[:amount] - params[:amount] + ) + add_refund_to_charge(refund, charge) + end + + if params[:application_fee] + charge[:application_fee] = params[:application_fee] + end + + charge[:captured] = true + charge + end + + def refund_charge(route, method_url, params, headers) + charge = get_charge(route, method_url, params, headers) + + new_refund( + route, + method_url, + params.merge(:charge => charge[:id]), + headers + ) + end + + private + + def ensure_required_params(params) + if params[:amount].nil? + require_param(:amount) + elsif params[:currency].nil? + require_param(:currency) + elsif non_integer_charge_amount?(params) + raise Stripe::InvalidRequestError.new("Invalid integer: #{params[:amount]}", 'amount', http_status: 400) + elsif non_positive_charge_amount?(params) + raise Stripe::InvalidRequestError.new('Invalid positive integer', 'amount', http_status: 400) + elsif params[:source].nil? && params[:customer].nil? + raise Stripe::InvalidRequestError.new('Must provide source or customer.', nil, http_status: nil) + end + end + + def non_integer_charge_amount?(params) + params[:amount] && !params[:amount].is_a?(Integer) + end + + def non_positive_charge_amount?(params) + params[:amount] && params[:amount] < 1 + end + + def allowed_params(params) + allowed = [:description, :metadata, :receipt_email, :fraud_details, :shipping, :destination] + + # This is a workaround for the way the Stripe API sends params even when they aren't modified. + # Stipe will include those params even when they aren't modified. + allowed << :fee_details if params.has_key?(:fee_details) && params[:fee_details].nil? + allowed << :source if params.has_key?(:source) && params[:source].empty? + if params.has_key?(:refunds) && (params[:refunds].empty? || + params[:refunds].has_key?(:data) && params[:refunds][:data].nil?) + allowed << :refunds + end + if params.has_key?(:payment_method_details) && (params[:payment_method_details].empty? || + params[:payment_method_details].has_key?(:card) && (params[:payment_method_details][:card].empty? || + params[:payment_method_details][:card].has_key?(:checks) && params[:payment_method_details][:card][:checks].empty?)) + allowed << :payment_method_details + end + + allowed + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/checkout_session.rb b/lib/stripe_mock/request_handlers/checkout_session.rb new file mode 100644 index 0000000..c0d408b --- /dev/null +++ b/lib/stripe_mock/request_handlers/checkout_session.rb @@ -0,0 +1,179 @@ +module StripeMock + module RequestHandlers + module Checkout + module Session + def Session.included(klass) + klass.add_handler 'post /v1/checkout/sessions', :new_session + klass.add_handler 'get /v1/checkout/sessions', :list_checkout_sessions + klass.add_handler 'get /v1/checkout/sessions/([^/]*)', :get_checkout_session + klass.add_handler 'get /v1/checkout/sessions/([^/]*)/line_items', :list_line_items + end + + def new_session(route, method_url, params, headers) + id = params[:id] || new_id('cs') + + [:cancel_url, :success_url].each do |p| + require_param(p) if params[p].nil? || params[p].empty? + end + + line_items = nil + if params[:line_items] + line_items = params[:line_items].each_with_index.map do |line_item, i| + throw Stripe::InvalidRequestError("Quantity is required. Add `quantity` to `line_items[#{i}]`") unless line_item[:quantity] + unless line_item[:price] || line_item[:price_data] || (line_item[:amount] && line_item[:currency] && line_item[:name]) + throw Stripe::InvalidRequestError("Price or amount and currency is required. Add `price`, `price_data`, or `amount`, `currency` and `name` to `line_items[#{i}]`") + end + { + id: new_id("li"), + price: if line_item[:price] + line_item[:price] + elsif line_item[:price_data] + new_price(nil, nil, line_item[:price_data], nil)[:id] + else + new_price(nil, nil, { + unit_amount: line_item[:amount], + currency: line_item[:currency], + product_data: { + name: line_item[:name] + } + }, nil)[:id] + end, + quantity: line_item[:quantity] + } + end + end + + amount = nil + currency = nil + if line_items + amount = 0 + + line_items.each do |line_item| + price = prices[line_item[:price]] + + if price.nil? + raise StripeMock::StripeMockError.new("Price not found for ID: #{line_item[:price]}") + end + + amount += (price[:unit_amount] * line_item[:quantity]) + end + + currency = prices[line_items.first[:price]][:currency] + end + + payment_status = "unpaid" + payment_intent = nil + setup_intent = nil + case params[:mode] + when nil, "payment" + params[:customer] ||= new_customer(nil, nil, {email: params[:customer_email]}, nil)[:id] + require_param(:line_items) if params[:line_items].nil? || params[:line_items].empty? + payment_intent = new_payment_intent(nil, nil, { + amount: amount, + currency: currency, + customer: params[:customer], + payment_method_options: params[:payment_method_options], + payment_method_types: params[:payment_method_types] + }.merge(params[:payment_intent_data] || {}), nil)[:id] + checkout_session_line_items[id] = line_items + when "setup" + if !params[:line_items].nil? && !params[:line_items].empty? + throw Stripe::InvalidRequestError.new("You cannot pass `line_items` in `setup` mode", :line_items, http_status: 400) + end + setup_intent = new_setup_intent(nil, nil, { + customer: params[:customer], + payment_method_options: params[:payment_method_options], + payment_method_types: params[:payment_method_types] + }.merge(params[:setup_intent_data] || {}), nil)[:id] + payment_status = "no_payment_required" + when "subscription" + params[:customer] ||= new_customer(nil, nil, {email: params[:customer_email]}, nil)[:id] + require_param(:line_items) if params[:line_items].nil? || params[:line_items].empty? + checkout_session_line_items[id] = line_items + else + throw Stripe::InvalidRequestError.new("Invalid mode: must be one of payment, setup, or subscription", :mode, http_status: 400) + end + + checkout_sessions[id] = { + id: id, + object: "checkout.session", + allow_promotion_codes: nil, + amount_subtotal: amount, + amount_total: amount, + automatic_tax: { + enabled: false, + status: nil + }, + billing_address_collection: nil, + cancel_url: params[:cancel_url], + client_reference_id: nil, + currency: currency, + customer: params[:customer], + customer_details: nil, + customer_email: params[:customer_email], + livemode: false, + locale: nil, + metadata: params[:metadata], + mode: params[:mode], + payment_intent: payment_intent, + payment_method_options: params[:payment_method_options], + payment_method_types: params[:payment_method_types], + payment_status: payment_status, + setup_intent: setup_intent, + shipping: nil, + shipping_address_collection: nil, + submit_type: nil, + subscription: nil, + success_url: params[:success_url], + total_details: nil, + url: URI.join(StripeMock.checkout_base, id).to_s + } + end + + def list_checkout_sessions(route, method_url, params, headers) + Data.mock_list_object(checkout_sessions.values) + end + + def get_checkout_session(route, method_url, params, headers) + route =~ method_url + checkout_session = assert_existence :checkout_session, $1, checkout_sessions[$1] + + checkout_session = checkout_session.clone + if params[:expand]&.include?('setup_intent') && checkout_session[:setup_intent] + checkout_session[:setup_intent] = setup_intents[checkout_session[:setup_intent]] + end + checkout_session + end + + def list_line_items(route, method_url, params, headers) + route =~ method_url + checkout_session = assert_existence :checkout_session, $1, checkout_sessions[$1] + + case checkout_session[:mode] + when "payment", "subscription" + line_items = assert_existence :checkout_session_line_items, $1, checkout_session_line_items[$1] + line_items.map do |line_item| + price = prices[line_item[:price]].clone + + if price.nil? + raise StripeMock::StripeMockError.new("Price not found for ID: #{line_item[:price]}") + end + + { + id: line_item[:id], + object: "item", + amount_subtotal: price[:unit_amount] * line_item[:quantity], + amount_total: price[:unit_amount] * line_item[:quantity], + currency: price[:currency], + price: price.clone, + quantity: line_item[:quantity] + } + end + else + throw Stripe::InvalidRequestError("Only payment and subscription sessions have line items") + end + end + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/country_spec.rb b/lib/stripe_mock/request_handlers/country_spec.rb new file mode 100644 index 0000000..6c681e8 --- /dev/null +++ b/lib/stripe_mock/request_handlers/country_spec.rb @@ -0,0 +1,22 @@ +module StripeMock + module RequestHandlers + module CountrySpec + + def CountrySpec.included(klass) + klass.add_handler 'get /v1/country_specs/(.*)', :retrieve_country_spec + end + + def retrieve_country_spec(route, method_url, params, headers) + route =~ method_url + + unless ["AT", "AU", "BE", "CA", "DE", "DK", "ES", "FI", "FR", "GB", "IE", "IT", "JP", "LU", "NL", "NO", "SE", "SG", "US"].include?($1) + raise Stripe::InvalidRequestError.new("#{$1} is not currently supported by Stripe.", $1.to_s) + end + + country_spec[$1] ||= Data.mock_country_spec($1) + + assert_existence :country_spec, $1, country_spec[$1] + end + end + end +end \ No newline at end of file diff --git a/lib/stripe_mock/request_handlers/coupons.rb b/lib/stripe_mock/request_handlers/coupons.rb new file mode 100644 index 0000000..3b4bd34 --- /dev/null +++ b/lib/stripe_mock/request_handlers/coupons.rb @@ -0,0 +1,35 @@ +module StripeMock + module RequestHandlers + module Coupons + + def Coupons.included(klass) + klass.add_handler 'post /v1/coupons', :new_coupon + klass.add_handler 'get /v1/coupons/(.*)', :get_coupon + klass.add_handler 'delete /v1/coupons/(.*)', :delete_coupon + klass.add_handler 'get /v1/coupons', :list_coupons + end + + def new_coupon(route, method_url, params, headers) + params[:id] ||= new_id('coupon') + raise Stripe::InvalidRequestError.new('Missing required param: duration', 'coupon', http_status: 400) unless params[:duration] + raise Stripe::InvalidRequestError.new('You must pass currency when passing amount_off', 'coupon', http_status: 400) if params[:amount_off] && !params[:currency] + coupons[ params[:id] ] = Data.mock_coupon({amount_off: nil, percent_off:nil}.merge(params)) + end + + def get_coupon(route, method_url, params, headers) + route =~ method_url + assert_existence :coupon, $1, coupons[$1] + end + + def delete_coupon(route, method_url, params, headers) + route =~ method_url + assert_existence :coupon, $1, coupons.delete($1) + end + + def list_coupons(route, method_url, params, headers) + Data.mock_list_object(coupons.values, params) + end + + end + end +end diff --git a/lib/stripe_mock/request_handlers/customers.rb b/lib/stripe_mock/request_handlers/customers.rb new file mode 100644 index 0000000..32946ab --- /dev/null +++ b/lib/stripe_mock/request_handlers/customers.rb @@ -0,0 +1,154 @@ +module StripeMock + module RequestHandlers + module Customers + + def Customers.included(klass) + klass.add_handler 'post /v1/customers', :new_customer + klass.add_handler 'post /v1/customers/([^/]*)', :update_customer + klass.add_handler 'get /v1/customers/([^/]*)', :get_customer + klass.add_handler 'delete /v1/customers/([^/]*)', :delete_customer + klass.add_handler 'get /v1/customers', :list_customers + klass.add_handler 'delete /v1/customers/([^/]*)/discount', :delete_customer_discount + end + + def new_customer(route, method_url, params, headers) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + params[:id] ||= new_id('cus') + sources = [] + + if params[:source] + new_card = + if params[:source].is_a?(Hash) + unless params[:source][:object] && params[:source][:number] && params[:source][:exp_month] && params[:source][:exp_year] + raise Stripe::InvalidRequestError.new('You must supply a valid card', nil, http_status: 400) + end + card_from_params(params[:source]) + else + get_card_or_bank_by_token(params.delete(:source)) + end + sources << new_card + params[:default_source] = sources.first[:id] + end + + customers[stripe_account] ||= {} + customers[stripe_account][params[:id]] = Data.mock_customer(sources, params) + + if params[:plan] + plan_id = params[:plan].to_s + plan = assert_existence :plan, plan_id, plans[plan_id] + + if params[:default_source].nil? && params[:trial_end].nil? && plan[:trial_period_days].nil? && plan[:amount] != 0 + raise Stripe::InvalidRequestError.new('You must supply a valid card', nil, http_status: 400) + end + + subscription = Data.mock_subscription({ id: new_id('su') }) + subscription = resolve_subscription_changes(subscription, [plan], customers[stripe_account][params[:id]], params) + add_subscription_to_customer(customers[stripe_account][params[:id]], subscription) + subscriptions[subscription[:id]] = subscription + elsif params[:trial_end] + raise Stripe::InvalidRequestError.new('Received unknown parameter: trial_end', nil, http_status: 400) + end + + if params[:coupon] + coupon = coupons[params[:coupon]] + assert_existence :coupon, params[:coupon], coupon + add_coupon_to_object(customers[stripe_account][params[:id]], coupon) + end + + customers[stripe_account][params[:id]] + end + + def update_customer(route, method_url, params, headers) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + route =~ method_url + cus = assert_existence :customer, $1, customers[stripe_account][$1] + + # get existing and pending metadata + metadata = cus.delete(:metadata) || {} + metadata_updates = params.delete(:metadata) || {} + + # Delete those params if their value is nil. Workaround of the problematic way Stripe serialize objects + params.delete(:sources) if params[:sources] && params[:sources][:data].nil? + params.delete(:subscriptions) if params[:subscriptions] && params[:subscriptions][:data].nil? + # Delete those params if their values aren't valid. Workaround of the problematic way Stripe serialize objects + if params[:sources] && !params[:sources][:data].nil? + params.delete(:sources) unless params[:sources][:data].any?{ |v| !!v[:type]} + end + if params[:subscriptions] && !params[:subscriptions][:data].nil? + params.delete(:subscriptions) unless params[:subscriptions][:data].any?{ |v| !!v[:type]} + end + cus.merge!(params) + cus[:metadata] = {**metadata, **metadata_updates} + + if params[:source] + if params[:source].is_a?(String) + new_card = get_card_or_bank_by_token(params.delete(:source)) + elsif params[:source].is_a?(Stripe::Token) + new_card = get_card_or_bank_by_token(params[:source][:id]) + elsif params[:source].is_a?(Hash) + unless params[:source][:object] && params[:source][:number] && params[:source][:exp_month] && params[:source][:exp_year] + raise Stripe::InvalidRequestError.new('You must supply a valid card', nil, http_status: 400) + end + new_card = card_from_params(params.delete(:source)) + end + add_card_to_object(:customer, new_card, cus, true) + cus[:default_source] = new_card[:id] + end + + if params[:coupon] + if params[:coupon] == '' + delete_coupon_from_object(cus) + else + coupon = coupons[params[:coupon]] + assert_existence :coupon, params[:coupon], coupon + + add_coupon_to_object(cus, coupon) + end + end + + cus + end + + def delete_customer(route, method_url, params, headers) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + route =~ method_url + assert_existence :customer, $1, customers[stripe_account][$1] + + customers[stripe_account][$1] = { + id: customers[stripe_account][$1][:id], + deleted: true + } + end + + def get_customer(route, method_url, params, headers) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + route =~ method_url + customer = assert_existence :customer, $1, customers[stripe_account][$1] + + customer = customer.clone + if params[:expand] == ['default_source'] && customer[:sources][:data] + customer[:default_source] = customer[:sources][:data].detect do |source| + source[:id] == customer[:default_source] + end + end + + customer + end + + def list_customers(route, method_url, params, headers) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + Data.mock_list_object(customers[stripe_account]&.values, params) + end + + def delete_customer_discount(route, method_url, params, headers) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + route =~ method_url + cus = assert_existence :customer, $1, customers[stripe_account][$1] + + cus[:discount] = nil + + cus + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/disputes.rb b/lib/stripe_mock/request_handlers/disputes.rb new file mode 100644 index 0000000..beec4c5 --- /dev/null +++ b/lib/stripe_mock/request_handlers/disputes.rb @@ -0,0 +1,35 @@ +module StripeMock + module RequestHandlers + module Disputes + + def Disputes.included(klass) + klass.add_handler 'get /v1/disputes/(.*)', :get_dispute + klass.add_handler 'post /v1/disputes/(.*)/close', :close_dispute + klass.add_handler 'post /v1/disputes/(.*)', :update_dispute + klass.add_handler 'get /v1/disputes', :list_disputes + end + + def get_dispute(route, method_url, params, headers) + route =~ method_url + assert_existence :dispute, $1, disputes[$1] + end + + def update_dispute(route, method_url, params, headers) + dispute = get_dispute(route, method_url, params, headers) + dispute.merge!(params) + dispute + end + + def close_dispute(route, method_url, params, headers) + dispute = get_dispute(route, method_url, params, headers) + dispute.merge!({:status => 'lost'}) + dispute + end + + def list_disputes(route, method_url, params, headers) + Data.mock_list_object(disputes.values, params) + end + + end + end +end diff --git a/lib/stripe_mock/request_handlers/ephemeral_key.rb b/lib/stripe_mock/request_handlers/ephemeral_key.rb new file mode 100644 index 0000000..c0fef51 --- /dev/null +++ b/lib/stripe_mock/request_handlers/ephemeral_key.rb @@ -0,0 +1,13 @@ +module StripeMock + module RequestHandlers + module EphemeralKey + def self.included(klass) + klass.add_handler 'post /v1/ephemeral_keys', :create_ephemeral_key + end + + def create_ephemeral_key(route, method_url, params, headers) + Data.mock_ephemeral_key(**params) + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/events.rb b/lib/stripe_mock/request_handlers/events.rb new file mode 100644 index 0000000..8999aa5 --- /dev/null +++ b/lib/stripe_mock/request_handlers/events.rb @@ -0,0 +1,48 @@ +module StripeMock + module RequestHandlers + module Events + + def Events.included(klass) + klass.add_handler 'get /v1/events/(.*)', :retrieve_event + klass.add_handler 'get /v1/events', :list_events + end + + def retrieve_event(route, method_url, params, headers) + route =~ method_url + assert_existence :event, $1, events[$1] + end + + def list_events(route, method_url, params, headers) + values = filter_by_created(events.values, params: params) + Data.mock_list_object(values, params) + end + + private + + def filter_by_created(event_list, params:) + if params[:created].nil? + return event_list + end + + if params[:created].is_a?(Hash) + if params[:created][:gt] + event_list = event_list.select { |event| event[:created] > params[:created][:gt].to_i } + end + if params[:created][:gte] + event_list = event_list.select { |event| event[:created] >= params[:created][:gte].to_i } + end + if params[:created][:lt] + event_list = event_list.select { |event| event[:created] < params[:created][:lt].to_i } + end + if params[:created][:lte] + event_list = event_list.select { |event| event[:created] <= params[:created][:lte].to_i } + end + else + event_list = event_list.select { |event| event[:created] == params[:created].to_i } + end + event_list + end + + end + end +end diff --git a/lib/stripe_mock/request_handlers/express_login_links.rb b/lib/stripe_mock/request_handlers/express_login_links.rb new file mode 100644 index 0000000..8b5e97e --- /dev/null +++ b/lib/stripe_mock/request_handlers/express_login_links.rb @@ -0,0 +1,15 @@ +module StripeMock + module RequestHandlers + module ExpressLoginLinks + + def ExpressLoginLinks.included(klass) + klass.add_handler 'post /v1/accounts/(.*)/login_links', :new_account_login_link + end + + def new_account_login_link(route, method_url, params, headers) + route =~ method_url + Data.mock_express_login_link(params) + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/external_accounts.rb b/lib/stripe_mock/request_handlers/external_accounts.rb new file mode 100644 index 0000000..958914e --- /dev/null +++ b/lib/stripe_mock/request_handlers/external_accounts.rb @@ -0,0 +1,55 @@ +module StripeMock + module RequestHandlers + module ExternalAccounts + + def ExternalAccounts.included(klass) + klass.add_handler 'get /v1/accounts/(.*)/external_accounts', :retrieve_external_accounts + klass.add_handler 'post /v1/accounts/(.*)/external_accounts', :create_external_account + klass.add_handler 'post /v1/accounts/(.*)/external_accounts/(.*)/verify', :verify_external_account + klass.add_handler 'get /v1/accounts/(.*)/external_accounts/(.*)', :retrieve_external_account + klass.add_handler 'delete /v1/accounts/(.*)/external_accounts/(.*)', :delete_external_account + klass.add_handler 'post /v1/accounts/(.*)/external_accounts/(.*)', :update_external_account + end + + def create_external_account(route, method_url, params, headers) + route =~ method_url + add_external_account_to(:account, $1, params, accounts) + end + + def retrieve_external_accounts(route, method_url, params, headers) + route =~ method_url + retrieve_object_cards(:account, $1, accounts) + end + + def retrieve_external_account(route, method_url, params, headers) + route =~ method_url + account = assert_existence :account, $1, accounts[$1] + + assert_existence :card, $2, get_card(account, $2) + end + + def delete_external_account(route, method_url, params, headers) + route =~ method_url + delete_card_from(:account, $1, $2, accounts) + end + + def update_external_account(route, method_url, params, headers) + route =~ method_url + account = assert_existence :account, $1, accounts[$1] + + card = assert_existence :card, $2, get_card(account, $2) + card.merge!(params) + card + end + + def verify_external_account(route, method_url, params, headers) + route =~ method_url + account = assert_existence :account, $1, accounts[$1] + + external_account = assert_existence :bank_account, $2, verify_bank_account(account, $2) + external_account + end + + end + end +end diff --git a/lib/stripe_mock/request_handlers/helpers/bank_account_helpers.rb b/lib/stripe_mock/request_handlers/helpers/bank_account_helpers.rb new file mode 100644 index 0000000..ddf7f23 --- /dev/null +++ b/lib/stripe_mock/request_handlers/helpers/bank_account_helpers.rb @@ -0,0 +1,14 @@ +module StripeMock + module RequestHandlers + module Helpers + + def verify_bank_account(object, bank_account_id, class_name='Customer') + bank_accounts = object[:external_accounts] || object[:bank_accounts] || object[:sources] + bank_account = bank_accounts[:data].find{|acc| acc[:id] == bank_account_id } + return if bank_account.nil? + bank_account['status'] = 'verified' + bank_account + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/helpers/card_helpers.rb b/lib/stripe_mock/request_handlers/helpers/card_helpers.rb new file mode 100644 index 0000000..98d76a6 --- /dev/null +++ b/lib/stripe_mock/request_handlers/helpers/card_helpers.rb @@ -0,0 +1,127 @@ +module StripeMock + module RequestHandlers + module Helpers + + def get_card(object, card_id, class_name='Customer') + cards = object[:cards] || object[:sources] || object[:external_accounts] + card = cards[:data].find{|cc| cc[:id] == card_id } + if card.nil? + if class_name == 'Recipient' + msg = "#{class_name} #{object[:id]} does not have a card with ID #{card_id}" + raise Stripe::InvalidRequestError.new(msg, 'card', http_status: 404) + else + msg = "There is no source with ID #{card_id}" + raise Stripe::InvalidRequestError.new(msg, 'id', http_status: 404) + end + end + card + end + + def add_source_to_object(type, source, object, replace_current=false) + source[type] = object[:id] + sources = object[:sources] + + if replace_current && sources[:data] + sources[:data].delete_if {|source| source[:id] == object[:default_source]} + object[:default_source] = source[:id] + sources[:data] = [source] + else + sources[:total_count] = (sources[:total_count] || 0) + 1 + (sources[:data] ||= []) << source + end + object[:default_source] = source[:id] if object[:default_source].nil? + + source + end + + def add_card_to_object(type, card, object, replace_current=false) + card[type] = object[:id] + cards_or_sources = object[:cards] || object[:sources] || object[:external_accounts] + + is_customer = object.has_key?(:sources) + + if replace_current && cards_or_sources[:data] + cards_or_sources[:data].delete_if {|card| card[:id] == object[:default_card]} + object[:default_card] = card[:id] unless is_customer + object[:default_source] = card[:id] if is_customer + cards_or_sources[:data] = [card] + else + cards_or_sources[:total_count] = (cards_or_sources[:total_count] || 0) + 1 + (cards_or_sources[:data] ||= []) << card + end + + object[:default_card] = card[:id] if !is_customer && object[:default_card].nil? + object[:default_source] = card[:id] if is_customer && object[:default_source].nil? + + card + end + + def retrieve_object_cards(type, type_id, objects) + resource = assert_existence type, type_id, objects[type_id] + cards = resource[:cards] || resource[:sources] || resource[:external_accounts] + + Data.mock_list_object(cards[:data]) + end + + def delete_card_from(type, type_id, card_id, objects) + resource = assert_existence type, type_id, objects[type_id] + + assert_existence :card, card_id, get_card(resource, card_id) + + card = { id: card_id, deleted: true } + cards_or_sources = resource[:cards] || resource[:sources] || resource[:external_accounts] + cards_or_sources[:data].reject!{|cc| + cc[:id] == card[:id] + } + + is_customer = resource.has_key?(:sources) + new_default = cards_or_sources[:data].count > 0 ? cards_or_sources[:data].first[:id] : nil + resource[:default_card] = new_default unless is_customer + resource[:sources][:total_count] = cards_or_sources[:data].count if is_customer + resource[:default_source] = new_default if is_customer + card + end + + def add_source_to(type, type_id, params, objects) + resource = assert_existence type, type_id, objects[type_id] + + source = + if params[:card] + card_from_params(params[:card]) + elsif params[:bank_account] + get_bank_by_token(params[:bank_account]) + else + begin + get_card_by_token(params[:source]) + rescue Stripe::InvalidRequestError + get_bank_by_token(params[:source]) + end + end + source[:metadata].merge!(params[:metadata]) if params[:metadata] + add_source_to_object(type, source, resource) + end + + def add_card_to(type, type_id, params, objects) + resource = assert_existence type, type_id, objects[type_id] + + card = card_from_params(params[:card] || params[:source] || params[:external_accounts]) + add_card_to_object(type, card, resource) + end + + def validate_card(card) + [:exp_month, :exp_year].each do |field| + card[field] = card[field].to_i + end + card + end + + def card_from_params(attrs_or_token) + if attrs_or_token.is_a? Hash + attrs_or_token = generate_card_token(attrs_or_token) + end + card = get_card_by_token(attrs_or_token) + validate_card(card) + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/helpers/charge_helpers.rb b/lib/stripe_mock/request_handlers/helpers/charge_helpers.rb new file mode 100644 index 0000000..63270b0 --- /dev/null +++ b/lib/stripe_mock/request_handlers/helpers/charge_helpers.rb @@ -0,0 +1,16 @@ +module StripeMock + module RequestHandlers + module Helpers + + def add_refund_to_charge(refund, charge) + refunds = charge[:refunds] + refunds[:data] << refund + refunds[:total_count] = refunds[:data].count + + charge[:amount_refunded] = refunds[:data].reduce(0) {|sum, r| sum + r[:amount].to_i } + charge[:refunded] = true + end + + end + end +end diff --git a/lib/stripe_mock/request_handlers/helpers/coupon_helpers.rb b/lib/stripe_mock/request_handlers/helpers/coupon_helpers.rb new file mode 100644 index 0000000..0e5fcb9 --- /dev/null +++ b/lib/stripe_mock/request_handlers/helpers/coupon_helpers.rb @@ -0,0 +1,22 @@ +module StripeMock + module RequestHandlers + module Helpers + def add_coupon_to_object(object, coupon) + discount_attrs = {}.tap do |attrs| + attrs[object[:object]] = object[:id] + attrs[:coupon] = coupon + attrs[:start] = Time.now.to_i + attrs[:end] = (DateTime.now >> coupon[:duration_in_months].to_i).to_time.to_i if coupon[:duration] == 'repeating' + end + + object[:discount] = Stripe::Discount.construct_from(discount_attrs) + object + end + + def delete_coupon_from_object(object) + object[:discount] = nil + object + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/helpers/external_account_helpers.rb b/lib/stripe_mock/request_handlers/helpers/external_account_helpers.rb new file mode 100644 index 0000000..7b06821 --- /dev/null +++ b/lib/stripe_mock/request_handlers/helpers/external_account_helpers.rb @@ -0,0 +1,49 @@ +module StripeMock + module RequestHandlers + module Helpers + + def add_external_account_to(type, type_id, params, objects) + resource = assert_existence type, type_id, objects[type_id] + + source = + if params[:card] + card_from_params(params[:card]) + elsif params[:bank_account] + bank_from_params(params[:bank_account]) + else + begin + get_card_by_token(params[:external_account]) + rescue Stripe::InvalidRequestError + bank_from_params(params[:external_account]) + end + end + add_external_account_to_object(type, source, resource) + end + + def add_external_account_to_object(type, source, object, replace_current=false) + source[type] = object[:id] + accounts = object[:external_accounts] + + if replace_current && accounts[:data] + accounts[:data].delete_if {|source| source[:id] == object[:default_source]} + object[:default_source] = source[:id] + accounts[:data] = [source] + else + accounts[:total_count] = (accounts[:total_count] || 0) + 1 + (accounts[:data] ||= []) << source + end + object[:default_source] = source[:id] if object[:default_source].nil? + + source + end + + def bank_from_params(attrs_or_token) + if attrs_or_token.is_a? Hash + attrs_or_token = generate_bank_token(attrs_or_token) + end + get_bank_by_token(attrs_or_token) + end + + end + end +end diff --git a/lib/stripe_mock/request_handlers/helpers/subscription_helpers.rb b/lib/stripe_mock/request_handlers/helpers/subscription_helpers.rb new file mode 100644 index 0000000..33bfd37 --- /dev/null +++ b/lib/stripe_mock/request_handlers/helpers/subscription_helpers.rb @@ -0,0 +1,130 @@ +module StripeMock + module RequestHandlers + module Helpers + + def get_customer_subscription(customer, sub_id) + customer[:subscriptions][:data].find{|sub| sub[:id] == sub_id } + end + + def resolve_subscription_changes(subscription, plans, customer, options = {}) + subscription.merge!(custom_subscription_params(plans, customer, options)) + items = options[:items] + items = items.values if items.respond_to?(:values) + subscription[:items][:data] = plans.map do |plan| + matching_item = items && items.detect { |item| [item[:price], item[:plan]].include? plan[:id] } + if matching_item + matching_item[:quantity] ||= 1 + matching_item[:id] ||= new_id('si') + params = matching_item.merge(plan: plan) + params[:price] = plan if plan[:object] == "price" + Data.mock_subscription_item(params) + else + params = { plan: plan, id: new_id('si') } + params[:price] = plan if plan[:object] == "price" + Data.mock_subscription_item(params) + end + end + subscription + end + + def custom_subscription_params(plans, cus, options = {}) + verify_trial_end(options[:trial_end]) if options[:trial_end] + + plan = plans.first if plans.size == 1 + + now = Time.now.utc.to_i + created_time = options[:created] || now + start_time = options[:current_period_start] || now + params = { customer: cus[:id], current_period_start: start_time, created: created_time } + params.merge!({ :plan => (plans.size == 1 ? plans.first : nil) }) + keys_to_merge = /application_fee_percent|quantity|metadata|tax_percent|billing|days_until_due|default_tax_rates|pending_invoice_item_interval|default_payment_method|collection_method/ + params.merge! options.select {|k,v| k =~ keys_to_merge} + + if options[:cancel_at_period_end] == true + params.merge!(cancel_at_period_end: true, canceled_at: now) + elsif options[:cancel_at_period_end] == false + params.merge!(cancel_at_period_end: false, canceled_at: nil) + end + + # TODO: Implement coupon logic + + if (((plan && plan[:trial_period_days]) || 0) == 0 && options[:trial_end].nil?) || options[:trial_end] == "now" + end_time = options[:billing_cycle_anchor] || get_ending_time(start_time, plan) + params.merge!({status: 'active', current_period_end: end_time, trial_start: nil, trial_end: nil, billing_cycle_anchor: options[:billing_cycle_anchor] || created_time}) + else + end_time = options[:trial_end] || (Time.now.utc.to_i + plan[:trial_period_days]*86400) + params.merge!({status: 'trialing', current_period_end: end_time, trial_start: start_time, trial_end: end_time, billing_cycle_anchor: options[:billing_cycle_anchor] || created_time}) + end + + params + end + + def add_subscription_to_customer(cus, sub) + if sub[:trial_end].nil? || sub[:trial_end] == "now" + id = new_id('ch') + charges[id] = Data.mock_charge( + :id => id, + :customer => cus[:id], + :amount => (sub[:plan] ? sub[:plan][:amount] : total_items_amount(sub[:items][:data])) + ) + end + + if cus[:currency].nil? + cus[:currency] = sub[:items][:data][0][:plan][:currency] + elsif cus[:currency] != sub[:items][:data][0][:plan][:currency] + raise Stripe::InvalidRequestError.new( "Can't combine currencies on a single customer. This customer has had a subscription, coupon, or invoice item with currency #{cus[:currency]}", 'currency', http_status: 400) + end + cus[:subscriptions][:total_count] = (cus[:subscriptions][:total_count] || 0) + 1 + cus[:subscriptions][:data].unshift sub + end + + def delete_subscription_from_customer(cus, subscription) + cus[:subscriptions][:data].reject!{|sub| + sub[:id] == subscription[:id] + } + cus[:subscriptions][:total_count] -=1 + end + + # `intervals` is set to 1 when calculating current_period_end from current_period_start & plan + # `intervals` is set to 2 when calculating Stripe::Invoice.upcoming end from current_period_start & plan + def get_ending_time(start_time, plan, intervals = 1) + return start_time unless plan + + interval = plan[:interval] || plan.dig(:recurring, :interval) + interval_count = plan[:interval_count] || plan.dig(:recurring, :interval_count) || 1 + case interval + when "week" + start_time + (604800 * (interval_count) * intervals) + when "month" + (Time.at(start_time).to_datetime >> ((interval_count) * intervals)).to_time.to_i + when "year" + (Time.at(start_time).to_datetime >> (12 * intervals)).to_time.to_i # max period is 1 year + else + start_time + end + end + + def verify_trial_end(trial_end) + if trial_end != "now" + if !trial_end.is_a? Integer + raise Stripe::InvalidRequestError.new('Invalid timestamp: must be an integer', nil, http_status: 400) + elsif trial_end < Time.now.utc.to_i + raise Stripe::InvalidRequestError.new('Invalid timestamp: must be an integer Unix timestamp in the future', nil, http_status: 400) + elsif trial_end > Time.now.utc.to_i + 31557600*5 # five years + raise Stripe::InvalidRequestError.new('Invalid timestamp: can be no more than five years in the future', nil, http_status: 400) + end + end + end + + def total_items_amount(items) + total = 0 + items.each do |item| + quantity = item[:quantity] || 1 + amount = item[:plan][:unit_amount] || item[:plan][:amount] + total += quantity * amount + end + total + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/helpers/token_helpers.rb b/lib/stripe_mock/request_handlers/helpers/token_helpers.rb new file mode 100644 index 0000000..666ddcb --- /dev/null +++ b/lib/stripe_mock/request_handlers/helpers/token_helpers.rb @@ -0,0 +1,44 @@ +module StripeMock + module RequestHandlers + module Helpers + + def generate_bank_token(bank_params = {}) + token = new_id 'btok' + bank_params[:id] = new_id 'bank_account' + @bank_tokens[token] = Data.mock_bank_account bank_params + token + end + + def generate_card_token(card_params = {}) + token = new_id 'tok' + card_params[:id] = new_id 'cc' + @card_tokens[token] = Data.mock_card symbolize_names(card_params) + token + end + + def get_bank_by_token(token) + if token.nil? || @bank_tokens[token].nil? + Data.mock_bank_account + else + @bank_tokens.delete(token) + end + end + + def get_card_by_token(token) + if token.nil? || @card_tokens[token].nil? + # TODO: Make this strict + msg = "Invalid token id: #{token}" + raise Stripe::InvalidRequestError.new(msg, 'tok', http_status: 404) + else + @card_tokens.delete(token) + end + end + + def get_card_or_bank_by_token(token) + token_id = token['id'] || token + @card_tokens[token_id] || @bank_tokens[token_id] || raise(Stripe::InvalidRequestError.new("Invalid token id: #{token_id}", 'tok', http_status: 404)) + end + + end + end +end diff --git a/lib/stripe_mock/request_handlers/invoice_items.rb b/lib/stripe_mock/request_handlers/invoice_items.rb new file mode 100644 index 0000000..49d2eea --- /dev/null +++ b/lib/stripe_mock/request_handlers/invoice_items.rb @@ -0,0 +1,45 @@ +module StripeMock + module RequestHandlers + module InvoiceItems + + def InvoiceItems.included(klass) + klass.add_handler 'post /v1/invoiceitems', :new_invoice_item + klass.add_handler 'post /v1/invoiceitems/(.*)', :update_invoice_item + klass.add_handler 'get /v1/invoiceitems/(.*)', :get_invoice_item + klass.add_handler 'get /v1/invoiceitems', :list_invoice_items + klass.add_handler 'delete /v1/invoiceitems/(.*)', :delete_invoice_item + end + + def new_invoice_item(route, method_url, params, headers) + params[:id] ||= new_id('ii') + invoice_items[params[:id]] = Data.mock_invoice_item(params) + end + + def update_invoice_item(route, method_url, params, headers) + route =~ method_url + list_item = assert_existence :list_item, $1, invoice_items[$1] + list_item.merge!(params) + end + + def delete_invoice_item(route, method_url, params, headers) + route =~ method_url + assert_existence :list_item, $1, invoice_items[$1] + + invoice_items[$1] = { + id: invoice_items[$1][:id], + deleted: true + } + end + + def list_invoice_items(route, method_url, params, headers) + Data.mock_list_object(invoice_items.values, params) + end + + def get_invoice_item(route, method_url, params, headers) + route =~ method_url + assert_existence :invoice_item, $1, invoice_items[$1] + end + + end + end +end diff --git a/lib/stripe_mock/request_handlers/invoices.rb b/lib/stripe_mock/request_handlers/invoices.rb new file mode 100644 index 0000000..6530966 --- /dev/null +++ b/lib/stripe_mock/request_handlers/invoices.rb @@ -0,0 +1,183 @@ +module StripeMock + module RequestHandlers + module Invoices + + def Invoices.included(klass) + klass.add_handler 'post /v1/invoices', :new_invoice + klass.add_handler 'get /v1/invoices/upcoming', :upcoming_invoice + klass.add_handler 'get /v1/invoices/(.*)/lines', :get_invoice_line_items + klass.add_handler 'get /v1/invoices/(.*)', :get_invoice + klass.add_handler 'get /v1/invoices', :list_invoices + klass.add_handler 'post /v1/invoices/(.*)/pay', :pay_invoice + klass.add_handler 'post /v1/invoices/(.*)', :update_invoice + end + + def new_invoice(route, method_url, params, headers) + id = new_id('in') + invoice_item = Data.mock_line_item() + invoices[id] = Data.mock_invoice([invoice_item], params.merge(:id => id)) + end + + def update_invoice(route, method_url, params, headers) + route =~ method_url + params.delete(:lines) if params[:lines] + assert_existence :invoice, $1, invoices[$1] + invoices[$1].merge!(params) + end + + def list_invoices(route, method_url, params, headers) + params[:offset] ||= 0 + params[:limit] ||= 10 + + result = invoices.clone + + if params[:customer] + result.delete_if { |k,v| v[:customer] != params[:customer] } + end + + Data.mock_list_object(result.values, params) + end + + def get_invoice(route, method_url, params, headers) + route =~ method_url + assert_existence :invoice, $1, invoices[$1] + end + + def get_invoice_line_items(route, method_url, params, headers) + route =~ method_url + assert_existence :invoice, $1, invoices[$1] + invoices[$1][:lines] + end + + def pay_invoice(route, method_url, params, headers) + route =~ method_url + assert_existence :invoice, $1, invoices[$1] + charge = invoice_charge(invoices[$1]) + invoices[$1].merge!(:paid => true, :attempted => true, :charge => charge[:id]) + end + + def upcoming_invoice(route, method_url, params, headers = {}) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + route =~ method_url + raise Stripe::InvalidRequestError.new('Missing required param: customer if subscription is not provided', nil, http_status: 400) if params[:customer].nil? && params[:subscription].nil? + raise Stripe::InvalidRequestError.new('When previewing changes to a subscription, you must specify either `subscription` or `subscription_items`', nil, http_status: 400) if !params[:subscription_proration_date].nil? && params[:subscription].nil? && params[:subscription_plan].nil? + raise Stripe::InvalidRequestError.new('Cannot specify proration date without specifying a subscription', nil, http_status: 400) if !params[:subscription_proration_date].nil? && params[:subscription].nil? + + customer = customers[stripe_account][params[:customer]] + assert_existence :customer, params[:customer], customer + + raise Stripe::InvalidRequestError.new("No upcoming invoices for customer: #{customer[:id]}", nil, http_status: 404) if customer[:subscriptions][:data].length == 0 + + subscription = + if params[:subscription] + customer[:subscriptions][:data].select{|s|s[:id] == params[:subscription]}.first + else + customer[:subscriptions][:data].min_by { |sub| sub[:current_period_end] } + end + + if params[:subscription_proration_date] && !((subscription[:current_period_start]..subscription[:current_period_end]) === params[:subscription_proration_date]) + raise Stripe::InvalidRequestError.new('Cannot specify proration date outside of current subscription period', nil, http_status: 400) + end + + prorating = false + subscription_proration_date = nil + subscription_plan_id = params[:subscription_plan] || subscription[:plan][:id] + subscription_quantity = params[:subscription_quantity] || subscription[:quantity] + if subscription_plan_id != subscription[:plan][:id] || subscription_quantity != subscription[:quantity] + prorating = true + invoice_date = Time.now.to_i + subscription_plan = assert_existence :plan, subscription_plan_id, plans[subscription_plan_id.to_s] + preview_subscription = Data.mock_subscription + preview_subscription = resolve_subscription_changes(preview_subscription, [subscription_plan], customer, { trial_end: params[:subscription_trial_end] }) + preview_subscription[:id] = subscription[:id] + preview_subscription[:quantity] = subscription_quantity + subscription_proration_date = params[:subscription_proration_date] || Time.now + else + preview_subscription = subscription + invoice_date = subscription[:current_period_end] + end + + invoice_lines = [] + + if prorating + unused_amount = ( + subscription[:plan][:amount].to_f * + subscription[:quantity] * + (subscription[:current_period_end] - subscription_proration_date.to_i) / (subscription[:current_period_end] - subscription[:current_period_start]) + ).ceil + + invoice_lines << Data.mock_line_item( + id: new_id('ii'), + amount: -unused_amount, + description: 'Unused time', + plan: subscription[:plan], + period: { + start: subscription_proration_date.to_i, + end: subscription[:current_period_end] + }, + quantity: subscription[:quantity], + proration: true + ) + + preview_plan = assert_existence :plan, params[:subscription_plan], plans[params[:subscription_plan]] + if preview_plan[:interval] == subscription[:plan][:interval] && preview_plan[:interval_count] == subscription[:plan][:interval_count] && params[:subscription_trial_end].nil? + remaining_amount = preview_plan[:amount] * subscription_quantity * (subscription[:current_period_end] - subscription_proration_date.to_i) / (subscription[:current_period_end] - subscription[:current_period_start]) + invoice_lines << Data.mock_line_item( + id: new_id('ii'), + amount: remaining_amount, + description: 'Remaining time', + plan: preview_plan, + period: { + start: subscription_proration_date.to_i, + end: subscription[:current_period_end] + }, + quantity: subscription_quantity, + proration: true + ) + end + end + + subscription_line = get_mock_subscription_line_item(preview_subscription) + invoice_lines << subscription_line + + Data.mock_invoice(invoice_lines, + id: new_id('in'), + customer: customer[:id], + discount: customer[:discount], + created: invoice_date, + starting_balance: customer[:account_balance], + subscription: preview_subscription[:id], + period_start: prorating ? invoice_date : preview_subscription[:current_period_start], + period_end: prorating ? invoice_date : preview_subscription[:current_period_end], + next_payment_attempt: preview_subscription[:current_period_end] + 3600 ) + end + + private + + def get_mock_subscription_line_item(subscription) + Data.mock_line_item( + id: subscription[:id], + type: "subscription", + plan: subscription[:plan], + amount: subscription[:status] == 'trialing' ? 0 : subscription[:plan][:amount] * subscription[:quantity], + discountable: true, + quantity: subscription[:quantity], + period: { + start: subscription[:current_period_end], + end: get_ending_time(subscription[:current_period_start], subscription[:plan], 2) + }) + end + + ## charge the customer on the invoice, if one does not exist, create + #anonymous charge + def invoice_charge(invoice) + begin + new_charge(nil, nil, {customer: invoice[:customer]["id"], amount: invoice[:amount_due], currency: StripeMock.default_currency}, nil) + rescue Stripe::InvalidRequestError + new_charge(nil, nil, {source: generate_card_token, amount: invoice[:amount_due], currency: StripeMock.default_currency}, nil) + end + end + + end + end +end diff --git a/lib/stripe_mock/request_handlers/orders.rb b/lib/stripe_mock/request_handlers/orders.rb new file mode 100644 index 0000000..39dc298 --- /dev/null +++ b/lib/stripe_mock/request_handlers/orders.rb @@ -0,0 +1,80 @@ +module StripeMock + module RequestHandlers + module Orders + + def Orders.included(klass) + klass.add_handler 'post /v1/orders', :new_order + klass.add_handler 'post /v1/orders/(.*)/pay', :pay_order + klass.add_handler 'post /v1/orders/(.*)', :update_order + klass.add_handler 'get /v1/orders/(.*)', :get_order + klass.add_handler 'get /v1/orders', :list_orders + end + + def new_order(route, method_url, params, headers) + params[:id] ||= new_id('or') + order_items = [] + + unless params[:currency].to_s.size == 3 + raise Stripe::InvalidRequestError.new('You must supply a currency', nil, http_status: 400) + end + + if params[:items] + unless params[:items].is_a? Array + raise Stripe::InvalidRequestError.new('You must supply a list of items', nil, http_status: 400) + end + + unless params[:items].first.is_a? Hash + raise Stripe::InvalidRequestError.new('You must supply an item', nil, http_status: 400) + end + end + + orders[ params[:id] ] = Data.mock_order(order_items, params) + + orders[ params[:id] ] + end + + def update_order(route, method_url, params, headers) + route =~ method_url + order = assert_existence :order, $1, orders[$1] + + if params[:metadata] + if params[:metadata].empty? + order[:metadata] = {} + else + order[:metadata].merge(params[:metadata]) + end + end + + if %w(created paid canceled fulfilled returned).include? params[:status] + order[:status] = params[:status] + end + order + end + + def get_order(route, method_url, params, headers) + route =~ method_url + assert_existence :order, $1, orders[$1] + end + + def pay_order(route, method_url, params, headers) + route =~ method_url + order = assert_existence :order, $1, orders[$1] + + if params[:source].blank? && params[:customer].blank? + raise Stripe::InvalidRequestError.new('You must supply a source or customer', nil, http_status: 400) + end + + charge_id = new_id('ch') + charges[charge_id] = Data.mock_charge(id: charge_id) + order[:charge] = charge_id + order[:status] = "paid" + order + end + + def list_orders(route, method_url, params, headers) + Data.mock_list_object(orders.values, params) + end + + end + end +end diff --git a/lib/stripe_mock/request_handlers/payment_intents.rb b/lib/stripe_mock/request_handlers/payment_intents.rb new file mode 100644 index 0000000..5cefdd3 --- /dev/null +++ b/lib/stripe_mock/request_handlers/payment_intents.rb @@ -0,0 +1,187 @@ +module StripeMock + module RequestHandlers + module PaymentIntents + ALLOWED_PARAMS = [:description, :metadata, :receipt_email, :shipping, :destination, :payment_method, :payment_method_types, :setup_future_usage, :transfer_data, :amount, :currency] + + def PaymentIntents.included(klass) + klass.add_handler 'post /v1/payment_intents', :new_payment_intent + klass.add_handler 'get /v1/payment_intents', :get_payment_intents + klass.add_handler 'get /v1/payment_intents/(.*)', :get_payment_intent + klass.add_handler 'post /v1/payment_intents/(.*)/confirm', :confirm_payment_intent + klass.add_handler 'post /v1/payment_intents/(.*)/capture', :capture_payment_intent + klass.add_handler 'post /v1/payment_intents/(.*)/cancel', :cancel_payment_intent + klass.add_handler 'post /v1/payment_intents/(.*)', :update_payment_intent + end + + def new_payment_intent(route, method_url, params, headers) + id = new_id('pi') + + ensure_payment_intent_required_params(params) + status = case params[:amount] + when 3184 then 'requires_action' + when 3178 then 'requires_payment_method' + when 3055 then 'requires_capture' + else + 'succeeded' + end + last_payment_error = params[:amount] == 3178 ? last_payment_error_generator(code: 'card_declined', decline_code: 'insufficient_funds', message: 'Not enough funds.') : nil + payment_intents[id] = Data.mock_payment_intent( + params.merge( + id: id, + status: status, + last_payment_error: last_payment_error + ) + ) + + if params[:confirm] && status == 'succeeded' + payment_intents[id] = succeeded_payment_intent(payment_intents[id]) + end + + payment_intents[id].clone + end + + def update_payment_intent(route, method_url, params, headers) + route =~ method_url + id = $1 + + payment_intent = assert_existence :payment_intent, id, payment_intents[id] + payment_intents[id] = Util.rmerge(payment_intent, params.select{ |k,v| ALLOWED_PARAMS.include?(k)}) + end + + def get_payment_intents(route, method_url, params, headers) + params[:offset] ||= 0 + params[:limit] ||= 10 + + clone = payment_intents.clone + + if params[:customer] + clone.delete_if { |k,v| v[:customer] != params[:customer] } + end + + Data.mock_list_object(clone.values, params) + end + + def get_payment_intent(route, method_url, params, headers) + route =~ method_url + payment_intent_id = $1 || params[:payment_intent] + payment_intent = assert_existence :payment_intent, payment_intent_id, payment_intents[payment_intent_id] + + payment_intent = payment_intent.clone + payment_intent + end + + def capture_payment_intent(route, method_url, params, headers) + route =~ method_url + payment_intent = assert_existence :payment_intent, $1, payment_intents[$1] + + succeeded_payment_intent(payment_intent) + end + + def confirm_payment_intent(route, method_url, params, headers) + route =~ method_url + payment_intent = assert_existence :payment_intent, $1, payment_intents[$1] + + if params[:payment_method] + payment_intent[:payment_method] = params[:payment_method] + end + + succeeded_payment_intent(payment_intent) + end + + def cancel_payment_intent(route, method_url, params, headers) + route =~ method_url + payment_intent = assert_existence :payment_intent, $1, payment_intents[$1] + + payment_intent[:status] = 'canceled' + payment_intent + end + + private + + def ensure_payment_intent_required_params(params) + if params[:amount].nil? + require_param(:amount) + elsif params[:currency].nil? + require_param(:currency) + elsif non_integer_charge_amount?(params) + raise Stripe::InvalidRequestError.new("Invalid integer: #{params[:amount]}", 'amount', http_status: 400) + elsif non_positive_charge_amount?(params) + raise Stripe::InvalidRequestError.new('Invalid positive integer', 'amount', http_status: 400) + end + end + + def non_integer_charge_amount?(params) + params[:amount] && !params[:amount].is_a?(Integer) + end + + def non_positive_charge_amount?(params) + params[:amount] && params[:amount] < 1 + end + + def last_payment_error_generator(code: nil, message: nil, decline_code: nil) + { + code: code, + doc_url: "https://stripe.com/docs/error-codes/payment-intent-authentication-failure", + message: message, + decline_code: decline_code, + payment_method: { + id: "pm_1EwXFA2eZvKYlo2C0tlY091l", + object: "payment_method", + billing_details: { + address: { + city: nil, + country: nil, + line1: nil, + line2: nil, + postal_code: nil, + state: nil + }, + email: nil, + name: "seller_08072019090000", + phone: nil + }, + card: { + brand: "visa", + checks: { + address_line1_check: nil, + address_postal_code_check: nil, + cvc_check: "unchecked" + }, + country: "US", + exp_month: 12, + exp_year: 2021, + fingerprint: "LQBhEmJnItuj3mxf", + funding: "credit", + generated_from: nil, + last4: "1629", + three_d_secure_usage: { + supported: true + }, + wallet: nil + }, + created: 1563208900, + customer: nil, + livemode: false, + metadata: {}, + type: "card" + }, + type: "invalid_request_error" + } + end + + def succeeded_payment_intent(payment_intent) + payment_intent[:status] = 'succeeded' + btxn = new_balance_transaction('txn', { source: payment_intent[:id] }) + + payment_intent[:charges][:data] << Data.mock_charge( + balance_transaction: btxn, + amount: payment_intent[:amount], + currency: payment_intent[:currency], + payment_method: payment_intent[:payment_method] + ) + + payment_intent + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/payment_methods.rb b/lib/stripe_mock/request_handlers/payment_methods.rb new file mode 100644 index 0000000..301e2be --- /dev/null +++ b/lib/stripe_mock/request_handlers/payment_methods.rb @@ -0,0 +1,124 @@ +module StripeMock + module RequestHandlers + module PaymentMethods + ALLOWED_PARAMS = [:customer, :type] + + def PaymentMethods.included(klass) + klass.add_handler 'post /v1/payment_methods', :new_payment_method + klass.add_handler 'get /v1/payment_methods/(.*)', :get_payment_method + klass.add_handler 'get /v1/payment_methods', :get_payment_methods + klass.add_handler 'post /v1/payment_methods/(.*)/attach', :attach_payment_method + klass.add_handler 'post /v1/payment_methods/(.*)/detach', :detach_payment_method + klass.add_handler 'post /v1/payment_methods/(.*)', :update_payment_method + end + + # post /v1/payment_methods + def new_payment_method(route, method_url, params, headers) + id = new_id('pm') + + ensure_payment_method_required_params(params) + + payment_methods[id] = Data.mock_payment_method( + params.merge( + id: id + ) + ) + + payment_methods[id].clone + end + + # + # params: {:type=>"card", :customer=>"test_cus_3"} + # + # get /v1/payment_methods/:id + def get_payment_method(route, method_url, params, headers) + id = method_url.match(route)[1] || params[:payment_method] + payment_method = assert_existence :payment_method, id, payment_methods[id] + + payment_method.clone + end + + # get /v1/payment_methods + def get_payment_methods(route, method_url, params, headers) + params[:offset] ||= 0 + params[:limit] ||= 10 + + clone = payment_methods.clone + + if params[:customer] + clone.delete_if { |_k, v| v[:customer] != params[:customer] } + end + + Data.mock_list_object(clone.values, params) + end + + # post /v1/payment_methods/:id/attach + def attach_payment_method(route, method_url, params, headers) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + allowed_params = [:customer] + + id = method_url.match(route)[1] + + assert_existence :customer, params[:customer], customers[stripe_account][params[:customer]] + + payment_method = assert_existence :payment_method, id, payment_methods[id] + payment_methods[id] = Util.rmerge(payment_method, params.select { |k, _v| allowed_params.include?(k) }) + payment_methods[id].clone + end + + # post /v1/payment_methods/:id/detach + def detach_payment_method(route, method_url, params, headers) + id = method_url.match(route)[1] + + payment_method = assert_existence :payment_method, id, payment_methods[id] + payment_method[:customer] = nil + + payment_method.clone + end + + # post /v1/payment_methods/:id + def update_payment_method(route, method_url, params, headers) + allowed_params = [:billing_details, :card, :metadata] + disallowed_params = params.keys - allowed_params + unless disallowed_params.empty? + raise Stripe::InvalidRequestError.new("Received unknown parameter: #{disallowed_params.first}", disallowed_params.first) + end + + id = method_url.match(route)[1] + + payment_method = assert_existence :payment_method, id, payment_methods[id] + + if payment_method[:customer].nil? + raise Stripe::InvalidRequestError.new( + 'You must save this PaymentMethod to a customer before you can update it.', + nil, + http_status: 400 + ) + end + + payment_methods[id] = + Util.rmerge(payment_method, params.select { |k, _v| allowed_params.include?(k)} ) + + payment_methods[id].clone + end + + private + + def ensure_payment_method_required_params(params) + require_param(:type) if params[:type].nil? + + if invalid_type?(params[:type]) + raise Stripe::InvalidRequestError.new( + 'Invalid type: must be one of card, ideal or sepa_debit', + nil, + http_status: 400 + ) + end + end + + def invalid_type?(type) + !%w(card ideal sepa_debit).include?(type) + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/payouts.rb b/lib/stripe_mock/request_handlers/payouts.rb new file mode 100644 index 0000000..c23b833 --- /dev/null +++ b/lib/stripe_mock/request_handlers/payouts.rb @@ -0,0 +1,32 @@ +module StripeMock + module RequestHandlers + module Payouts + + def Payouts.included(klass) + klass.add_handler 'post /v1/payouts', :new_payout + klass.add_handler 'get /v1/payouts', :list_payouts + klass.add_handler 'get /v1/payouts/(.*)', :get_payout + end + + def new_payout(route, method_url, params, headers) + id = new_id('po') + + unless params[:amount].is_a?(Integer) || (params[:amount].is_a?(String) && /^\d+$/.match(params[:amount])) + raise Stripe::InvalidRequestError.new("Invalid integer: #{params[:amount]}", 'amount', http_status: 400) + end + + payouts[id] = Data.mock_payout(params.merge :id => id) + end + + def list_payouts(route, method_url, params, headers) + Data.mock_list_object(payouts.clone.values, params) + end + + def get_payout(route, method_url, params, headers) + route =~ method_url + assert_existence :payout, $1, payouts[$1] + payouts[$1] ||= Data.mock_payout(:id => $1) + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/plans.rb b/lib/stripe_mock/request_handlers/plans.rb new file mode 100644 index 0000000..feae495 --- /dev/null +++ b/lib/stripe_mock/request_handlers/plans.rb @@ -0,0 +1,42 @@ +module StripeMock + module RequestHandlers + module Plans + + def Plans.included(klass) + klass.add_handler 'post /v1/plans', :new_plan + klass.add_handler 'post /v1/plans/(.*)', :update_plan + klass.add_handler 'get /v1/plans/(.*)', :get_plan + klass.add_handler 'delete /v1/plans/(.*)', :delete_plan + klass.add_handler 'get /v1/plans', :list_plans + end + + def new_plan(route, method_url, params, headers) + params[:id] ||= new_id('plan') + validate_create_plan_params(params) + plans[ params[:id] ] = Data.mock_plan(params) + end + + def update_plan(route, method_url, params, headers) + route =~ method_url + assert_existence :plan, $1, plans[$1] + plans[$1].merge!(params) + end + + def get_plan(route, method_url, params, headers) + route =~ method_url + assert_existence :plan, $1, plans[$1] + end + + def delete_plan(route, method_url, params, headers) + route =~ method_url + assert_existence :plan, $1, plans.delete($1) + end + + def list_plans(route, method_url, params, headers) + limit = params[:limit] ? params[:limit] : 10 + Data.mock_list_object(plans.values.first(limit), params.merge!(limit: limit)) + end + + end + end +end diff --git a/lib/stripe_mock/request_handlers/prices.rb b/lib/stripe_mock/request_handlers/prices.rb new file mode 100644 index 0000000..46146a4 --- /dev/null +++ b/lib/stripe_mock/request_handlers/prices.rb @@ -0,0 +1,50 @@ +module StripeMock + module RequestHandlers + module Prices + + def Prices.included(klass) + klass.add_handler 'post /v1/prices', :new_price + klass.add_handler 'post /v1/prices/(.*)', :update_price + klass.add_handler 'get /v1/prices/(.*)', :get_price + klass.add_handler 'get /v1/prices', :list_prices + end + + def new_price(route, method_url, params, headers) + params[:id] ||= new_id('price') + + if params[:product_data] + params[:product] = create_product(nil, nil, params[:product_data], nil)[:id] unless params[:product] + params.delete(:product_data) + end + + validate_create_price_params(params) + prices[ params[:id] ] = Data.mock_price(params) + end + + def update_price(route, method_url, params, headers) + route =~ method_url + assert_existence :price, $1, prices[$1] + prices[$1].merge!(params) + end + + def get_price(route, method_url, params, headers) + route =~ method_url + assert_existence :price, $1, prices[$1] + end + + def list_prices(route, method_url, params, headers) + limit = params[:limit] ? params[:limit] : 10 + price_data = prices.values + validate_list_prices_params(params) + + if params.key?(:lookup_keys) + price_data.select! do |price| + params[:lookup_keys].include?(price[:lookup_key]) + end + end + + Data.mock_list_object(price_data.first(limit), params.merge!(limit: limit)) + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/products.rb b/lib/stripe_mock/request_handlers/products.rb new file mode 100644 index 0000000..a75461c --- /dev/null +++ b/lib/stripe_mock/request_handlers/products.rb @@ -0,0 +1,44 @@ +module StripeMock + module RequestHandlers + module Products + def self.included(base) + base.add_handler 'post /v1/products', :create_product + base.add_handler 'get /v1/products/(.*)', :retrieve_product + base.add_handler 'post /v1/products/(.*)', :update_product + base.add_handler 'get /v1/products', :list_products + base.add_handler 'delete /v1/products/(.*)', :destroy_product + end + + def create_product(_route, _method_url, params, _headers) + params[:id] ||= new_id('prod') + validate_create_product_params(params) + products[params[:id]] = Data.mock_product(params) + end + + def retrieve_product(route, method_url, _params, _headers) + id = method_url.match(route).captures.first + assert_existence :product, id, products[id] + end + + def update_product(route, method_url, params, _headers) + id = method_url.match(route).captures.first + product = assert_existence :product, id, products[id] + + product.merge!(params) + end + + def list_products(_route, _method_url, params, _headers) + limit = params[:limit] || 10 + Data.mock_list_object(products.values.take(limit), params) + end + + def destroy_product(route, method_url, _params, _headers) + id = method_url.match(route).captures.first + assert_existence :product, id, products[id] + + products.delete(id) + { id: id, object: 'product', deleted: true } + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/recipients.rb b/lib/stripe_mock/request_handlers/recipients.rb new file mode 100644 index 0000000..956e136 --- /dev/null +++ b/lib/stripe_mock/request_handlers/recipients.rb @@ -0,0 +1,60 @@ +module StripeMock + module RequestHandlers + module Recipients + + def Recipients.included(klass) + klass.add_handler 'post /v1/recipients', :new_recipient + klass.add_handler 'post /v1/recipients/(.*)', :update_recipient + klass.add_handler 'get /v1/recipients/(.*)', :get_recipient + end + + def new_recipient(route, method_url, params, headers) + params[:id] ||= new_id('rp') + cards = [] + + if params[:name].nil? + raise StripeMock::StripeMockError.new("Missing required parameter name for recipients.") + end + + if params[:type].nil? + raise StripeMock::StripeMockError.new("Missing required parameter type for recipients.") + end + + unless %w(individual corporation).include?(params[:type]) + raise StripeMock::StripeMockError.new("Type must be either individual or corporation..") + end + + if params[:bank_account] + params[:active_account] = get_bank_by_token(params.delete(:bank_account)) + end + + if params[:card] + cards << get_card_by_token(params.delete(:card)) + params[:default_card] = cards.first[:id] + end + + recipients[ params[:id] ] = Data.mock_recipient(cards, params) + recipients[ params[:id] ] + end + + def update_recipient(route, method_url, params, headers) + route =~ method_url + recipient = assert_existence :recipient, $1, recipients[$1] + recipient.merge!(params) + + if params[:card] + new_card = get_card_by_token(params.delete(:card)) + add_card_to_object(:recipient, new_card, recipient, true) + recipient[:default_card] = new_card[:id] + end + + recipient + end + + def get_recipient(route, method_url, params, headers) + route =~ method_url + assert_existence :recipient, $1, recipients[$1] + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/refunds.rb b/lib/stripe_mock/request_handlers/refunds.rb new file mode 100644 index 0000000..62aabde --- /dev/null +++ b/lib/stripe_mock/request_handlers/refunds.rb @@ -0,0 +1,102 @@ +module StripeMock + module RequestHandlers + module Refunds + + def Refunds.included(klass) + klass.add_handler 'post /v1/refunds', :new_refund + klass.add_handler 'get /v1/refunds', :get_refunds + klass.add_handler 'get /v1/refunds/(.*)', :get_refund + klass.add_handler 'post /v1/refunds/(.*)', :update_refund + end + + def new_refund(route, method_url, params, headers) + if headers && headers[:idempotency_key] + params[:idempotency_key] = headers[:idempotency_key] + if refunds.any? + original_refund = refunds.values.find { |c| c[:idempotency_key] == headers[:idempotency_key]} + return refunds[original_refund[:id]] if original_refund + end + end + + if params[:payment_intent] + payment_intent = assert_existence( + :payment_intent, + params[:payment_intent], + payment_intents[params[:payment_intent]] + ) + charge = {} + else + charge = assert_existence :charge, params[:charge], charges[params[:charge]] + payment_intent = {} + end + params[:amount] ||= payment_intent[:amount] + params[:amount] ||= charge[:amount] + id = new_id('re') + bal_trans_params = { + amount: params[:amount] * -1, + source: id, + type: 'refund' + } + balance_transaction_id = new_balance_transaction('txn', bal_trans_params) + refund = Data.mock_refund params.merge( + :balance_transaction => balance_transaction_id, + :id => id, + :charge => charge[:id], + ) + add_refund_to_charge(refund, charge) unless charge.empty? + refunds[id] = refund + + if params[:expand] == ['balance_transaction'] + refunds[id][:balance_transaction] = + balance_transactions[balance_transaction_id] + end + refund + end + + def update_refund(route, method_url, params, headers) + route =~ method_url + id = $1 + + refund = assert_existence :refund, id, refunds[id] + allowed = allowed_refund_params(params) + disallowed = params.keys - allowed + if disallowed.count > 0 + raise Stripe::InvalidRequestError.new("Received unknown parameters: #{disallowed.join(', ')}" , '', http_status: 400) + end + + refunds[id] = Util.rmerge(refund, params) + end + + def get_refunds(route, method_url, params, headers) + params[:offset] ||= 0 + params[:limit] ||= 10 + + clone = refunds.clone + + Data.mock_list_object(clone.values, params) + end + + def get_refund(route, method_url, params, headers) + route =~ method_url + refund_id = $1 || params[:refund] + assert_existence :refund, refund_id, refunds[refund_id] + end + + private + + def ensure_refund_required_params(params) + if non_integer_charge_amount?(params) + raise Stripe::InvalidRequestError.new("Invalid integer: #{params[:amount]}", 'amount', http_status: 400) + elsif non_positive_charge_amount?(params) + raise Stripe::InvalidRequestError.new('Invalid positive integer', 'amount', http_status: 400) + elsif params[:charge].nil? + raise Stripe::InvalidRequestError.new('Must provide the identifier of the charge to refund.', nil) + end + end + + def allowed_refund_params(params) + [:metadata] + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/setup_intents.rb b/lib/stripe_mock/request_handlers/setup_intents.rb new file mode 100644 index 0000000..8c6cd42 --- /dev/null +++ b/lib/stripe_mock/request_handlers/setup_intents.rb @@ -0,0 +1,98 @@ +module StripeMock + module RequestHandlers + module SetupIntents + ALLOWED_PARAMS = [ + :confirm, + :customer, + :description, + :metadata, + :on_behalf_of, + :payment_method, + :payment_method_options, + :payment_method_types, + :return_url, + :usage + ] + + def SetupIntents.included(klass) + klass.add_handler 'post /v1/setup_intents', :new_setup_intent + klass.add_handler 'get /v1/setup_intents', :get_setup_intents + klass.add_handler 'get /v1/setup_intents/(.*)', :get_setup_intent + klass.add_handler 'post /v1/setup_intents/(.*)/confirm', :confirm_setup_intent + klass.add_handler 'post /v1/setup_intents/(.*)/cancel', :cancel_setup_intent + klass.add_handler 'post /v1/setup_intents/(.*)', :update_setup_intent + end + + def new_setup_intent(route, method_url, params, headers) + id = new_id('si') + + setup_intents[id] = Data.mock_setup_intent( + params.merge( + id: id + ) + ) + + setup_intents[id].clone + end + + def update_setup_intent(route, method_url, params, headers) + route =~ method_url + id = $1 + + setup_intent = assert_existence :setup_intent, id, setup_intents[id] + setup_intents[id] = Util.rmerge(setup_intent, params.select { |k, v| ALLOWED_PARAMS.include?(k) }) + end + + def get_setup_intents(route, method_url, params, headers) + params[:offset] ||= 0 + params[:limit] ||= 10 + + clone = setup_intents.clone + + if params[:customer] + clone.delete_if { |k, v| v[:customer] != params[:customer] } + end + + Data.mock_list_object(clone.values, params) + end + + def get_setup_intent(route, method_url, params, headers) + route =~ method_url + setup_intent_id = $1 || params[:setup_intent] + setup_intent = assert_existence :setup_intent, setup_intent_id, setup_intents[setup_intent_id] + + setup_intent = setup_intent.clone + + if params[:expand]&.include?("payment_method") + setup_intent[:payment_method] = assert_existence :payment_method, setup_intent[:payment_method], payment_methods[setup_intent[:payment_method]] + end + + setup_intent + end + + def capture_setup_intent(route, method_url, params, headers) + route =~ method_url + setup_intent = assert_existence :setup_intent, $1, setup_intents[$1] + + setup_intent[:status] = 'succeeded' + setup_intent + end + + def confirm_setup_intent(route, method_url, params, headers) + route =~ method_url + setup_intent = assert_existence :setup_intent, $1, setup_intents[$1] + + setup_intent[:status] = 'succeeded' + setup_intent + end + + def cancel_setup_intent(route, method_url, params, headers) + route =~ method_url + setup_intent = assert_existence :setup_intent, $1, setup_intents[$1] + + setup_intent[:status] = 'canceled' + setup_intent + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/sources.rb b/lib/stripe_mock/request_handlers/sources.rb new file mode 100644 index 0000000..3439691 --- /dev/null +++ b/lib/stripe_mock/request_handlers/sources.rb @@ -0,0 +1,61 @@ +module StripeMock + module RequestHandlers + module Sources + + def Sources.included(klass) + klass.add_handler 'get /v1/customers/(.*)/sources', :retrieve_sources + klass.add_handler 'post /v1/customers/(.*)/sources', :create_source + klass.add_handler 'post /v1/customers/(.*)/sources/(.*)/verify', :verify_source + klass.add_handler 'get /v1/customers/(.*)/sources/(.*)', :retrieve_source + klass.add_handler 'delete /v1/customers/(.*)/sources/(.*)', :delete_source + klass.add_handler 'post /v1/customers/(.*)/sources/(.*)', :update_source + end + + def create_source(route, method_url, params, headers) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + route =~ method_url + add_source_to(:customer, $1, params, customers[stripe_account]) + end + + def retrieve_sources(route, method_url, params, headers) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + route =~ method_url + retrieve_object_cards(:customer, $1, customers[stripe_account]) + end + + def retrieve_source(route, method_url, params, headers) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + route =~ method_url + customer = assert_existence :customer, $1, customers[stripe_account][$1] + + assert_existence :card, $2, get_card(customer, $2) + end + + def delete_source(route, method_url, params, headers) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + route =~ method_url + delete_card_from(:customer, $1, $2, customers[stripe_account]) + end + + def update_source(route, method_url, params, headers) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + route =~ method_url + customer = assert_existence :customer, $1, customers[stripe_account][$1] + + card = assert_existence :card, $2, get_card(customer, $2) + card.merge!(params) + card + end + + def verify_source(route, method_url, params, headers) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + route =~ method_url + customer = assert_existence :customer, $1, customers[stripe_account][$1] + + bank_account = assert_existence :bank_account, $2, verify_bank_account(customer, $2) + bank_account + end + + end + end +end diff --git a/lib/stripe_mock/request_handlers/subscription_items.rb b/lib/stripe_mock/request_handlers/subscription_items.rb new file mode 100644 index 0000000..1c33b4d --- /dev/null +++ b/lib/stripe_mock/request_handlers/subscription_items.rb @@ -0,0 +1,36 @@ +module StripeMock + module RequestHandlers + module SubscriptionItems + + def SubscriptionItems.included(klass) + klass.add_handler 'get /v1/subscription_items', :retrieve_subscription_items + klass.add_handler 'post /v1/subscription_items/([^/]*)', :update_subscription_item + klass.add_handler 'post /v1/subscription_items', :create_subscription_items + end + + def retrieve_subscription_items(route, method_url, params, headers) + route =~ method_url + + require_param(:subscription) unless params[:subscription] + + Data.mock_list_object(subscriptions_items, params) + end + + def create_subscription_items(route, method_url, params, headers) + params[:id] ||= new_id('si') + + require_param(:subscription) unless params[:subscription] + require_param(:plan) unless params[:plan] + + subscriptions_items[params[:id]] = Data.mock_subscription_item(params.merge(plan: plans[params[:plan]])) + end + + def update_subscription_item(route, method_url, params, headers) + route =~ method_url + + subscription_item = assert_existence :subscription_item, $1, subscriptions_items[$1] + subscription_item.merge!(params.merge(plan: plans[params[:plan]])) + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/subscriptions.rb b/lib/stripe_mock/request_handlers/subscriptions.rb new file mode 100644 index 0000000..d8c31d1 --- /dev/null +++ b/lib/stripe_mock/request_handlers/subscriptions.rb @@ -0,0 +1,359 @@ +module StripeMock + module RequestHandlers + module Subscriptions + + def Subscriptions.included(klass) + klass.add_handler 'get /v1/subscriptions', :retrieve_subscriptions + klass.add_handler 'post /v1/subscriptions', :create_subscription + klass.add_handler 'get /v1/subscriptions/(.*)', :retrieve_subscription + klass.add_handler 'post /v1/subscriptions/(.*)', :update_subscription + klass.add_handler 'delete /v1/subscriptions/(.*)', :cancel_subscription + + klass.add_handler 'post /v1/customers/(.*)/subscription(?:s)?', :create_customer_subscription + klass.add_handler 'get /v1/customers/(.*)/subscription(?:s)?/(.*)', :retrieve_customer_subscription + klass.add_handler 'get /v1/customers/(.*)/subscription(?:s)?', :retrieve_customer_subscriptions + klass.add_handler 'post /v1/customers/(.*)subscription(?:s)?/(.*)', :update_subscription + klass.add_handler 'delete /v1/customers/(.*)/subscription(?:s)?/(.*)', :cancel_subscription + end + + def retrieve_customer_subscription(route, method_url, params, headers) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + route =~ method_url + + customer = assert_existence :customer, $1, customers[stripe_account][$1] + subscription = get_customer_subscription(customer, $2) + + assert_existence :subscription, $2, subscription + end + + def retrieve_customer_subscriptions(route, method_url, params, headers) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + route =~ method_url + + customer = assert_existence :customer, $1, customers[stripe_account][$1] + customer[:subscriptions] + end + + def create_customer_subscription(route, method_url, params, headers) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + route =~ method_url + + subscription_plans = get_subscription_plans_from_params(params) + customer = assert_existence :customer, $1, customers[stripe_account][$1] + + if params[:source] + new_card = get_card_by_token(params.delete(:source)) + add_card_to_object(:customer, new_card, customer) + customer[:default_source] = new_card[:id] + end + + subscription = Data.mock_subscription({ id: (params[:id] || new_id('su')) }) + subscription = resolve_subscription_changes(subscription, subscription_plans, customer, params) + + # Ensure customer has card to charge if plan has no trial and is not free + # Note: needs updating for subscriptions with multiple plans + verify_card_present(customer, subscription_plans.first, subscription, params) + + if params[:coupon] + coupon_id = params[:coupon] + + # assert_existence returns 404 error code but Stripe returns 400 + # coupon = assert_existence :coupon, coupon_id, coupons[coupon_id] + + coupon = coupons[coupon_id] + + if coupon + add_coupon_to_object(subscription, coupon) + else + raise Stripe::InvalidRequestError.new("No such coupon: #{coupon_id}", 'coupon', http_status: 400) + end + end + + subscriptions[subscription[:id]] = subscription + add_subscription_to_customer(customer, subscription) + + subscriptions[subscription[:id]] + end + + def create_subscription(route, method_url, params, headers) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + if headers && headers[:idempotency_key] + if subscriptions.any? + original_subscription = subscriptions.values.find { |c| c[:idempotency_key] == headers[:idempotency_key]} + return subscriptions[original_subscription[:id]] if original_subscription + end + end + route =~ method_url + + subscription_plans = get_subscription_plans_from_params(params) + + customer = params[:customer] + customer_id = customer.is_a?(Stripe::Customer) ? customer[:id] : customer.to_s + customer = assert_existence :customer, customer_id, customers[stripe_account][customer_id] + + if params[:source] + new_card = get_card_by_token(params.delete(:source)) + add_card_to_object(:customer, new_card, customer) + customer[:default_source] = new_card[:id] + end + + allowed_params = %w(customer application_fee_percent coupon items metadata plan quantity source tax_percent trial_end trial_period_days current_period_start created prorate billing_cycle_anchor billing days_until_due idempotency_key enable_incomplete_payments cancel_at_period_end default_tax_rates payment_behavior pending_invoice_item_interval default_payment_method collection_method off_session trial_from_plan proration_behavior backdate_start_date transfer_data expand automatic_tax) + unknown_params = params.keys - allowed_params.map(&:to_sym) + if unknown_params.length > 0 + raise Stripe::InvalidRequestError.new("Received unknown parameter: #{unknown_params.join}", unknown_params.first.to_s, http_status: 400) + end + + subscription = Data.mock_subscription({ id: (params[:id] || new_id('su')) }) + subscription = resolve_subscription_changes(subscription, subscription_plans, customer, params) + if headers[:idempotency_key] + subscription[:idempotency_key] = headers[:idempotency_key] + end + + # Ensure customer has card to charge if plan has no trial and is not free + # Note: needs updating for subscriptions with multiple plans + verify_card_present(customer, subscription_plans.first, subscription, params) + + if params[:coupon] + coupon_id = params[:coupon] + + # assert_existence returns 404 error code but Stripe returns 400 + # coupon = assert_existence :coupon, coupon_id, coupons[coupon_id] + + coupon = coupons[coupon_id] + + if coupon + add_coupon_to_object(subscription, coupon) + else + raise Stripe::InvalidRequestError.new("No such coupon: #{coupon_id}", 'coupon', http_status: 400) + end + end + + if params[:trial_period_days] + subscription[:status] = 'trialing' + end + + if params[:payment_behavior] == 'default_incomplete' + subscription[:status] = 'incomplete' + end + + if params[:cancel_at_period_end] + subscription[:cancel_at_period_end] = true + subscription[:canceled_at] = Time.now.utc.to_i + end + + if params[:transfer_data] && !params[:transfer_data].empty? + throw Stripe::InvalidRequestError.new(missing_param_message("transfer_data[destination]")) unless params[:transfer_data][:destination] + subscription[:transfer_data] = params[:transfer_data].dup + subscription[:transfer_data][:amount_percent] ||= 100 + end + + if (s = params[:expand]&.find { |s| s.start_with? 'latest_invoice' }) + payment_intent = nil + unless subscription[:status] == 'trialing' + intent_status = subscription[:status] == 'incomplete' ? 'requires_payment_method' : 'succeeded' + intent = Data.mock_payment_intent({ + status: intent_status, + amount: subscription[:plan][:amount], + currency: subscription[:plan][:currency] + }) + payment_intent = s.include?('latest_invoice.payment_intent') ? intent : intent.id + end + invoice = Data.mock_invoice([], { payment_intent: payment_intent }) + subscription[:latest_invoice] = invoice + end + + subscriptions[subscription[:id]] = subscription + add_subscription_to_customer(customer, subscription) + + subscriptions[subscription[:id]] + end + + def retrieve_subscription(route, method_url, params, headers) + route =~ method_url + + assert_existence :subscription, $1, subscriptions[$1] + end + + def retrieve_subscriptions(route, method_url, params, headers) + # stripe_account = headers && headers[:stripe_account] || Stripe.api_key + route =~ method_url + + subs = subscriptions.values + + case params[:status] + when nil + subs = subs.filter {|subscription| subscription[:status] != "canceled"} + when "all" + # Include all subscriptions + else + subs = subs.filter {|subscription| subscription[:status] == params[:status]} + end + + Data.mock_list_object(subs, params) + end + + def update_subscription(route, method_url, params, headers) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + route =~ method_url + + if params[:billing_cycle_anchor] == 'now' + params[:billing_cycle_anchor] = Time.now.utc.to_i + end + + subscription_id = $2 ? $2 : $1 + subscription = assert_existence :subscription, subscription_id, subscriptions[subscription_id] + verify_active_status(subscription) + + customer_id = subscription[:customer] + customer = assert_existence :customer, customer_id, customers[stripe_account][customer_id] + + if params[:source] + new_card = get_card_by_token(params.delete(:source)) + add_card_to_object(:customer, new_card, customer) + customer[:default_source] = new_card[:id] + end + + subscription_plans = get_subscription_plans_from_params(params) + + # subscription plans are not being updated but load them for the response + if subscription_plans.empty? + subscription_plans = subscription[:items][:data].map { |item| item[:plan] } + end + + if params[:coupon] + coupon_id = params[:coupon] + + # assert_existence returns 404 error code but Stripe returns 400 + # coupon = assert_existence :coupon, coupon_id, coupons[coupon_id] + + coupon = coupons[coupon_id] + if coupon + add_coupon_to_object(subscription, coupon) + elsif coupon_id == "" + subscription[:discount] = nil + else + raise Stripe::InvalidRequestError.new("No such coupon: #{coupon_id}", 'coupon', http_status: 400) + end + end + + if params[:trial_period_days] + subscription[:status] = 'trialing' + end + + if params[:cancel_at_period_end] + subscription[:cancel_at_period_end] = true + subscription[:canceled_at] = Time.now.utc.to_i + elsif params.has_key?(:cancel_at_period_end) + subscription[:cancel_at_period_end] = false + subscription[:canceled_at] = nil + end + + params[:current_period_start] = subscription[:current_period_start] + params[:trial_end] = params[:trial_end] || subscription[:trial_end] + + plan_amount_was = subscription.dig(:plan, :amount) + + subscription = resolve_subscription_changes(subscription, subscription_plans, customer, params) + + verify_card_present(customer, subscription_plans.first, subscription, params) if plan_amount_was == 0 && subscription.dig(:plan, :amount) && subscription.dig(:plan, :amount) > 0 + + # delete the old subscription, replace with the new subscription + customer[:subscriptions][:data].reject! { |sub| sub[:id] == subscription[:id] } + customer[:subscriptions][:data] << subscription + + subscription + end + + def cancel_subscription(route, method_url, params, headers) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + route =~ method_url + + subscription_id = $2 ? $2 : $1 + subscription = assert_existence :subscription, subscription_id, subscriptions[subscription_id] + + customer_id = subscription[:customer] + customer = assert_existence :customer, customer_id, customers[stripe_account][customer_id] + + cancel_params = { canceled_at: Time.now.utc.to_i } + cancelled_at_period_end = (params[:at_period_end] == true) + if cancelled_at_period_end + cancel_params[:cancel_at_period_end] = true + else + cancel_params[:status] = 'canceled' + cancel_params[:cancel_at_period_end] = false + cancel_params[:ended_at] = Time.now.utc.to_i + end + + subscription.merge!(cancel_params) + + unless cancelled_at_period_end + delete_subscription_from_customer customer, subscription + end + + subscription + end + + private + + def get_subscription_plans_from_params(params) + plan_ids = if params[:plan] + [params[:plan].to_s] + elsif params[:items] + items = params[:items] + items = items.values if items.respond_to?(:values) + items.map { |item| item[:plan] ? item[:plan] : item[:price] } + else + [] + end + plan_ids.compact! + plan_ids.each do |plan_id| + assert_existence :plan, plan_id, plans[plan_id] + rescue Stripe::InvalidRequestError + assert_existence :price, plan_id, prices[plan_id] + end + plan_ids.map { |plan_id| plans[plan_id] || prices[plan_id]} + end + + # Ensure customer has card to charge unless one of the following criterias is met: + # 1) is in trial + # 2) is free + # 3) has billing set to send invoice + def verify_card_present(customer, plan, subscription, params={}) + return if customer[:default_source] + return if customer[:invoice_settings][:default_payment_method] + return if customer[:trial_end] + return if params[:trial_end] + return if params[:payment_behavior] == 'default_incomplete' + return if subscription[:default_payment_method] + + plan_trial_period_days = plan[:trial_period_days] || 0 + plan_has_trial = plan_trial_period_days != 0 || plan[:amount] == 0 || plan[:trial_end] + return if plan && plan_has_trial + + return if subscription && subscription[:trial_end] && subscription[:trial_end] != 'now' + + if subscription[:items] + trial = subscription[:items][:data].none? do |item| + plan = item[:plan] + (plan[:trial_period_days].nil? || plan[:trial_period_days] == 0) && + (plan[:trial_end].nil? || plan[:trial_end] == 'now') + end + return if trial + end + + return if params[:billing] == 'send_invoice' + + raise Stripe::InvalidRequestError.new('This customer has no attached payment source', nil, http_status: 400) + end + + def verify_active_status(subscription) + id, status = subscription.values_at(:id, :status) + + if status == 'canceled' + message = "No such subscription: #{id}" + raise Stripe::InvalidRequestError.new(message, 'subscription', http_status: 404) + end + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/tax_rates.rb b/lib/stripe_mock/request_handlers/tax_rates.rb new file mode 100644 index 0000000..212ad3f --- /dev/null +++ b/lib/stripe_mock/request_handlers/tax_rates.rb @@ -0,0 +1,36 @@ +module StripeMock + module RequestHandlers + module TaxRates + def TaxRates.included(klass) + klass.add_handler 'post /v1/tax_rates', :new_tax_rate + klass.add_handler 'post /v1/tax_rates/([^/]*)', :update_tax_rate + klass.add_handler 'get /v1/tax_rates/([^/]*)', :get_tax_rate + klass.add_handler 'get /v1/tax_rates', :list_tax_rates + end + + def update_tax_rate(route, method_url, params, headers) + route =~ method_url + rate = assert_existence :tax_rate, $1, tax_rates[$1] + rate.merge!(params) + rate + end + + def new_tax_rate(route, method_url, params, headers) + params[:id] ||= new_id('txr') + tax_rates[ params[:id] ] = Data.mock_tax_rate(params) + tax_rates[ params[:id] ] + end + + def list_tax_rates(route, method_url, params, headers) + Data.mock_list_object(tax_rates.values, params) + end + + def get_tax_rate(route, method_url, params, headers) + route =~ method_url + tax_rate = assert_existence :tax_rate, $1, tax_rates[$1] + tax_rate.clone + end + end + end +end + diff --git a/lib/stripe_mock/request_handlers/tokens.rb b/lib/stripe_mock/request_handlers/tokens.rb new file mode 100644 index 0000000..d46ecf3 --- /dev/null +++ b/lib/stripe_mock/request_handlers/tokens.rb @@ -0,0 +1,77 @@ +module StripeMock + module RequestHandlers + module Tokens + + def Tokens.included(klass) + klass.add_handler 'post /v1/tokens', :create_token + klass.add_handler 'get /v1/tokens/(.*)', :get_token + end + + def create_token(route, method_url, params, headers) + stripe_account = headers && headers[:stripe_account] || Stripe.api_key + + if params[:customer].nil? && params[:card].nil? && params[:bank_account].nil? + raise Stripe::InvalidRequestError.new('You must supply either a card, customer, or bank account to create a token.', nil, http_status: 400) + end + + cus_id = params[:customer] + + if cus_id && params[:source] + customer = assert_existence :customer, cus_id, customers[stripe_account][cus_id] + + # params[:card] is an id; grab it from the db + customer_card = get_card(customer, params[:source]) + assert_existence :card, params[:source], customer_card + elsif params[:card].is_a?(String) + customer = assert_existence :customer, cus_id, customers[stripe_account][cus_id] + + # params[:card] is an id; grab it from the db + customer_card = get_card(customer, params[:card]) + assert_existence :card, params[:card], customer_card + elsif params[:card] + # params[:card] is a hash of cc info; "Sanitize" the card number + params[:card][:fingerprint] = StripeMock::Util.fingerprint(params[:card][:number]) + params[:card][:last4] = params[:card][:number][-4,4] + customer_card = params[:card] + elsif params[:bank_account].is_a?(String) + customer = assert_existence :customer, cus_id, customers[stripe_account][cus_id] + + # params[:bank_account] is an id; grab it from the db + bank_account = verify_bank_account(customer, params[:bank_account]) + assert_existence :bank_account, params[:bank_account], bank_account + elsif params[:bank_account] + # params[:card] is a hash of cc info; "Sanitize" the card number + bank_account = params[:bank_account] + else + customer = assert_existence :customer, cus_id, customers[stripe_account][cus_id] || customers[Stripe.api_key][cus_id] + customer_card = get_card(customer, customer[:default_source]) + end + + if bank_account + token_id = generate_bank_token(bank_account.dup) + bank_account = @bank_tokens[token_id] + + Data.mock_bank_account_token(params.merge :id => token_id, :bank_account => bank_account) + else + token_id = generate_card_token(customer_card.dup) + card = @card_tokens[token_id] + + Data.mock_card_token(params.merge :id => token_id, :card => card) + end + end + + def get_token(route, method_url, params, headers) + route =~ method_url + # A Stripe token can be either a bank token or a card token + bank_or_card = @bank_tokens[$1] || @card_tokens[$1] + assert_existence :token, $1, bank_or_card + + if bank_or_card[:object] == 'card' + Data.mock_card_token(:id => $1, :card => bank_or_card) + elsif bank_or_card[:object] == 'bank_account' + Data.mock_bank_account_token(:id => $1, :bank_account => bank_or_card) + end + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/transfers.rb b/lib/stripe_mock/request_handlers/transfers.rb new file mode 100644 index 0000000..6e04ea3 --- /dev/null +++ b/lib/stripe_mock/request_handlers/transfers.rb @@ -0,0 +1,65 @@ +module StripeMock + module RequestHandlers + module Transfers + + def Transfers.included(klass) + klass.add_handler 'post /v1/transfers', :new_transfer + klass.add_handler 'get /v1/transfers', :get_all_transfers + klass.add_handler 'get /v1/transfers/(.*)', :get_transfer + klass.add_handler 'post /v1/transfers/(.*)/cancel', :cancel_transfer + end + + def get_all_transfers(route, method_url, params, headers) + extra_params = params.keys - [:created, :destination, :ending_before, + :limit, :starting_after, :transfer_group] + unless extra_params.empty? + raise Stripe::InvalidRequestError.new("Received unknown parameter: #{extra_params[0]}", extra_params[0].to_s, http_status: 400) + end + + if destination = params[:destination] + assert_existence :destination, destination, accounts[destination] + end + + _transfers = transfers.each_with_object([]) do |(_, transfer), array| + if destination + array << transfer if transfer[:destination] == destination + else + array << transfer + end + end + + if params[:limit] + _transfers = _transfers.first([params[:limit], _transfers.size].min) + end + + Data.mock_list_object(_transfers, params) + end + + def new_transfer(route, method_url, params, headers) + id = new_id('tr') + if params[:bank_account] + params[:account] = get_bank_by_token(params.delete(:bank_account)) + end + + unless params[:amount].is_a?(Integer) || (params[:amount].is_a?(String) && /^\d+$/.match(params[:amount])) + raise Stripe::InvalidRequestError.new("Invalid integer: #{params[:amount]}", 'amount', http_status: 400) + end + + transfers[id] = Data.mock_transfer(params.merge :id => id) + end + + def get_transfer(route, method_url, params, headers) + route =~ method_url + assert_existence :transfer, $1, transfers[$1] + transfers[$1] ||= Data.mock_transfer(:id => $1) + end + + def cancel_transfer(route, method_url, params, headers) + route =~ method_url + assert_existence :transfer, $1, transfers[$1] + t = transfers[$1] ||= Data.mock_transfer(:id => $1) + t.merge!({:status => "canceled"}) + end + end + end +end diff --git a/lib/stripe_mock/request_handlers/validators/param_validators.rb b/lib/stripe_mock/request_handlers/validators/param_validators.rb new file mode 100644 index 0000000..aa5f239 --- /dev/null +++ b/lib/stripe_mock/request_handlers/validators/param_validators.rb @@ -0,0 +1,147 @@ +module StripeMock + module RequestHandlers + module ParamValidators + + def already_exists_message(obj_class) + "#{obj_class.to_s.split("::").last} already exists." + end + + def not_found_message(obj_class, obj_id) + "No such #{obj_class.to_s.split("::").last.downcase}: #{obj_id}" + end + + def missing_param_message(attr_name) + "Missing required param: #{attr_name}." + end + + def invalid_integer_message(my_val) + "Invalid integer: #{my_val}" + end + + # + # ProductValidator + # + + + def validate_create_product_params(params) + params[:id] = params[:id].to_s + @base_strategy.create_product_params.keys.reject{ |k,_| k == :id }.each do |k| + raise Stripe::InvalidRequestError.new(missing_param_message(k), k) if params[k].nil? + end + + if products[ params[:id] ] + raise Stripe::InvalidRequestError.new(already_exists_message(Stripe::Product), :id) + end + end + + # + # PlanValidator + # + + def missing_plan_amount_message + "Plans require an `amount` parameter to be set." + end + + SUPPORTED_PLAN_INTERVALS = ["month", "year", "week", "day"] + + def invalid_plan_interval_message + "Invalid interval: must be one of day, month, week, or year" + end + + SUPPORTED_CURRENCIES = [ + "usd", "aed", "afn", "all", "amd", "ang", "aoa", "ars", "aud", "awg", "azn", "bam", "bbd", "bdt", "bgn", + "bif", "bmd", "bnd", "bob", "brl", "bsd", "bwp", "bzd", "cad", "cdf", "chf", "clp", "cny", "cop", "crc", + "cve", "czk", "djf", "dkk", "dop", "dzd", "egp", "etb", "eur", "fjd", "fkp", "gbp", "gel", "gip", "gmd", + "gnf", "gtq", "gyd", "hkd", "hnl", "hrk", "htg", "huf", "idr", "ils", "inr", "isk", "jmd", "jpy", "kes", + "kgs", "khr", "kmf", "krw", "kyd", "kzt", "lak", "lbp", "lkr", "lrd", "lsl", "mad", "mdl", "mga", "mkd", + "mmk", "mnt", "mop", "mro", "mur", "mvr", "mwk", "mxn", "myr", "mzn", "nad", "ngn", "nio", "nok", "npr", + "nzd", "pab", "pen", "pgk", "php", "pkr", "pln", "pyg", "qar", "ron", "rsd", "rub", "rwf", "sar", "sbd", + "scr", "sek", "sgd", "shp", "sll", "sos", "srd", "std", "szl", "thb", "tjs", "top", "try", "ttd", "twd", + "tzs", "uah", "ugx", "uyu", "uzs", "vnd", "vuv", "wst", "xaf", "xcd", "xof", "xpf", "yer", "zar", "zmw", + "eek", "lvl", "svc", "vef", "ltl" + ] + + def invalid_currency_message(my_val) + "Invalid currency: #{my_val.downcase}. Stripe currently supports these currencies: #{SUPPORTED_CURRENCIES.join(", ")}" + end + + def validate_create_plan_params(params) + plan_id = params[:id].to_s + product_id = params[:product] + + @base_strategy.create_plan_params.keys.each do |attr_name| + message = + if attr_name == :amount + "Plans require an `#{attr_name}` parameter to be set." + else + "Missing required param: #{attr_name}." + end + raise Stripe::InvalidRequestError.new(message, attr_name) if params[attr_name].nil? + end + + if plans[plan_id] + message = already_exists_message(Stripe::Plan) + raise Stripe::InvalidRequestError.new(message, :id) + end + + unless products[product_id] + message = not_found_message(Stripe::Product, product_id) + raise Stripe::InvalidRequestError.new(message, :product) + end + + unless SUPPORTED_PLAN_INTERVALS.include?(params[:interval]) + message = invalid_plan_interval_message + raise Stripe::InvalidRequestError.new(message, :interval) + end + + unless SUPPORTED_CURRENCIES.include?(params[:currency]) + message = invalid_currency_message(params[:currency]) + raise Stripe::InvalidRequestError.new(message, :currency) + end + + unless params[:amount].integer? + message = invalid_integer_message(params[:amount]) + raise Stripe::InvalidRequestError.new(message, :amount) + end + + end + + def validate_create_price_params(params) + price_id = params[:id].to_s + + require_param(:currency) unless params[:currency] + unless params[:product] || params[:product_data] + raise Stripe::InvalidRequestError("Requires product or product_data") + end + + product_id = params[:product] || create_product(nil, nil, params[:product_data], nil).id + + if prices[price_id] + message = already_exists_message(Stripe::Price) + raise Stripe::InvalidRequestError.new(message, :id) + end + + unless products[product_id] + message = not_found_message(Stripe::Product, product_id) + raise Stripe::InvalidRequestError.new(message, :product) + end + + unless SUPPORTED_CURRENCIES.include?(params[:currency]) + message = invalid_currency_message(params[:currency]) + raise Stripe::InvalidRequestError.new(message, :currency) + end + end + + def validate_list_prices_params(params) + if params[:lookup_keys] && !params[:lookup_keys].is_a?(Array) + raise Stripe::InvalidRequestError.new('Invalid array', :lookup_keys) + end + end + + def require_param(param_name) + raise Stripe::InvalidRequestError.new("Missing required param: #{param_name}.", param_name.to_s, http_status: 400) + end + + end + end +end diff --git a/lib/stripe_mock/server.rb b/lib/stripe_mock/server.rb new file mode 100644 index 0000000..6f2cda2 --- /dev/null +++ b/lib/stripe_mock/server.rb @@ -0,0 +1,93 @@ +require 'drb/drb' + +module StripeMock + class Server + def self.start_new(opts) + puts "Starting StripeMock server on port #{opts[:port] || 4999}" + + host = opts.fetch :host,'0.0.0.0' + port = opts.fetch :port, 4999 + + DRb.start_service "druby://#{host}:#{port}", Server.new + DRb.thread.join + end + + def initialize + self.clear_data + end + + def mock_request(*args, **kwargs) + begin + @instance.mock_request(*args, **kwargs) + rescue Stripe::InvalidRequestError => e + { + :error_raised => 'invalid_request', + :error_params => [ + e.message, e.param, { http_status: e.http_status, http_body: e.http_body, json_body: e.json_body} + ] + } + end + end + + def get_data(key) + @instance.send(key) + end + + def destroy_resource(type, id) + @instance.send(type).delete(id) + end + + def clear_data + @instance = Instance.new + end + + def set_debug(toggle) + @instance.debug = toggle + end + + def set_global_id_prefix(value) + StripeMock.global_id_prefix = value + end + + def global_id_prefix + StripeMock.global_id_prefix + end + + def generate_card_token(card_params) + @instance.generate_card_token(card_params) + end + + def generate_bank_token(recipient_params) + @instance.generate_bank_token(recipient_params) + end + + def generate_webhook_event(event_data) + @instance.generate_webhook_event(event_data) + end + + def set_conversion_rate(value) + @instance.conversion_rate = value + end + + def set_account_balance(value) + @instance.account_balance = value + end + + def error_queue + @instance.error_queue + end + + def debug? + @instance.debug + end + + def ping + true + end + + def upsert_stripe_object(object, attributes) + @instance.upsert_stripe_object(object, attributes) + end + + end +end diff --git a/lib/stripe_mock/test_strategies/base.rb b/lib/stripe_mock/test_strategies/base.rb new file mode 100644 index 0000000..09a8f85 --- /dev/null +++ b/lib/stripe_mock/test_strategies/base.rb @@ -0,0 +1,167 @@ +module StripeMock + module TestStrategies + class Base + + def list_products(limit) + Stripe::Product.list(limit: limit) + end + + def create_product(params = {}) + Stripe::Product.create create_product_params(params) + end + + def create_product_params(params = {}) + { + :id => 'stripe_mock_default_product_id', + :name => 'Default Product', + }.merge(params) + end + + def retrieve_product(product_id) + Stripe::Product.retrieve(product_id) + end + + def list_plans(limit) + Stripe::Plan.list(limit: limit) + end + + def create_plan(params = {}) + Stripe::Plan.create create_plan_params(params) + end + + def create_plan_params(params = {}) + { + :id => 'stripe_mock_default_plan_id', + :interval => 'month', + :currency => StripeMock.default_currency, + :product => nil, # need to override yourself to pass validations + :amount => 1337 + }.merge(params) + end + + def create_price(params = {}) + Stripe::Price.create create_price_params(params) + end + + def create_price_params(params = {}) + price_params = { + currency: StripeMock.default_currency, + }.merge(params) + unless price_params.key?(:product) || price_params.key?(:product_data) + price_params[:product_data] = { + name: 'Product created for price' + } + end + price_params + end + + def list_subscriptions(limit) + Stripe::Subscription.list(limit: limit) + end + + def generate_card_token(card_params = {}) + card_data = {:number => "4242424242424242", :exp_month => 9, :exp_year => (Time.now.year + 5), :cvc => "999", :tokenization_method => nil} + card = StripeMock::Util.card_merge(card_data, card_params) + card[:fingerprint] = StripeMock::Util.fingerprint(card[:number]) if StripeMock.state == 'local' + + stripe_token = Stripe::Token.create(:card => card) + stripe_token.id + end + + def generate_bank_token(bank_account_params = {}) + currency = bank_account_params[:currency] || StripeMock.default_currency + bank_account = { + :country => "US", + :currency => currency, + :account_holder_name => "Jane Austen", + :account_holder_type => "individual", + :routing_number => "110000000", + :account_number => "000123456789" + }.merge(bank_account_params) + bank_account[:fingerprint] = StripeMock::Util.fingerprint(bank_account[:account_number]) if StripeMock.state == 'local' + + stripe_token = Stripe::Token.create(:bank_account => bank_account) + stripe_token.id + end + + def create_coupon_params(params = {}) + currency = params[:currency] || StripeMock.default_currency + { + id: '10BUCKS', + amount_off: 1000, + currency: currency, + max_redemptions: 100, + metadata: { + created_by: 'admin_acct_1' + }, + duration: 'once' + }.merge(params) + end + + def create_coupon_percent_of_params(params = {}) + { + id: '25PERCENT', + percent_off: 25, + redeem_by: nil, + duration_in_months: 3, + duration: :repeating + }.merge(params) + end + + def create_checkout_session(params = {}) + Stripe::Checkout::Session.create create_checkout_session_params(params) + end + + def create_checkout_session_params(params = {}) + { + payment_method_types: ['card'], + line_items: params[:mode] == "setup" ? nil : [{ + name: 'T-shirt', + quantity: 1, + amount: 500, + currency: 'usd', + }], + cancel_url: "https://example.com/cancel", + success_url: "https://example.com/success", + }.merge(params) + end + + def complete_checkout_session(session, payment_method) + session = session.is_a?(Stripe::Checkout::Session) ? session : Stripe::Checkout::Session.retrieve(session) + payment_method = payment_method.is_a?(Stripe::PaymentMethod) ? payment_method : Stripe::PaymentMethod.retrieve(payment_method) + case session.mode + when "payment" + Stripe::PaymentIntent.retrieve(session.payment_intent).confirm(payment_method: payment_method.id) + when "setup" + Stripe::SetupIntent.update(session.setup_intent, {payment_method: payment_method.id}) + when "subscription" + line_items = Stripe::Checkout::Session.list_line_items(session.id) + Stripe::Subscription.create({ + customer: session.customer, + items: line_items.map do |line_item| + { + price: line_item.price.id, + quantity: line_item.quantity + } + end, + default_payment_method: payment_method.id + }) + end + end + + def create_coupon(params = {}) + Stripe::Coupon.create create_coupon_params(params) + end + + def delete_all_coupons + coupons = Stripe::Coupon.list + coupons.data.map(&:delete) if coupons.data.count > 0 + end + + def prepare_card_error + StripeMock.prepare_card_error(:card_error, :new_customer) if StripeMock.state == 'local' + end + + end + end +end diff --git a/lib/stripe_mock/test_strategies/live.rb b/lib/stripe_mock/test_strategies/live.rb new file mode 100644 index 0000000..258b207 --- /dev/null +++ b/lib/stripe_mock/test_strategies/live.rb @@ -0,0 +1,51 @@ +module StripeMock + module TestStrategies + class Live < Base + + def create_product(params={}) + params = create_product_params(params) + raise "create_product requires an :id" if params[:id].nil? + delete_product(params[:id]) + Stripe::Product.create params + end + + def delete_product(product_id) + product = Stripe::Product.retrieve(product_id) + Stripe::Plan.list(product: product_id).each(&:delete) if product.type == 'service' + product.delete + rescue Stripe::StripeError => e + # do nothing + end + + def create_plan(params={}) + raise "create_plan requires an :id" if params[:id].nil? + delete_plan(params[:id]) + Stripe::Plan.create create_plan_params(params) + end + + def delete_plan(plan_id) + plan = Stripe::Plan.retrieve(plan_id) + plan.delete + rescue Stripe::StripeError => e + # do nothing + end + + def create_coupon(params={}) + delete_coupon create_coupon_params(params)[:id] + super + end + + def delete_coupon(id) + coupon = Stripe::Coupon.retrieve(id) + coupon.delete + rescue Stripe::StripeError + # do nothing + end + + def upsert_stripe_object(object, attributes) + raise UnsupportedRequestError.new "Updating or inserting Stripe objects in Live mode not supported" + end + + end + end +end diff --git a/lib/stripe_mock/test_strategies/mock.rb b/lib/stripe_mock/test_strategies/mock.rb new file mode 100644 index 0000000..4241d78 --- /dev/null +++ b/lib/stripe_mock/test_strategies/mock.rb @@ -0,0 +1,31 @@ +module StripeMock + module TestStrategies + class Mock < Base + + def delete_product(product_id) + if StripeMock.state == 'remote' + StripeMock.client.destroy_resource('products', product_id) + elsif StripeMock.state == 'local' + StripeMock.instance.products.delete(product_id) + end + end + + def delete_plan(plan_id) + if StripeMock.state == 'remote' + StripeMock.client.destroy_resource('plans', plan_id) + elsif StripeMock.state == 'local' + StripeMock.instance.plans.delete(plan_id) + end + end + + def upsert_stripe_object(object, attributes = {}) + if StripeMock.state == 'remote' + StripeMock.client.upsert_stripe_object(object, attributes) + elsif StripeMock.state == 'local' + StripeMock.instance.upsert_stripe_object(object, attributes) + end + end + + end + end +end diff --git a/lib/stripe_mock/util.rb b/lib/stripe_mock/util.rb new file mode 100644 index 0000000..e776218 --- /dev/null +++ b/lib/stripe_mock/util.rb @@ -0,0 +1,44 @@ +module StripeMock + module Util + + def self.rmerge(desh_hash, source_hash) + return source_hash if desh_hash.nil? + return nil if source_hash.nil? + + desh_hash.merge(source_hash) do |key, oldval, newval| + if oldval.is_a?(Array) && newval.is_a?(Array) + oldval.fill(nil, oldval.length...newval.length) + oldval.zip(newval).map {|elems| + if elems[1].nil? + elems[0] + elsif elems[1].is_a?(Hash) && elems[1].is_a?(Hash) + rmerge(elems[0], elems[1]) + else + [elems[0], elems[1]].compact + end + }.flatten + elsif oldval.is_a?(Hash) && newval.is_a?(Hash) + rmerge(oldval, newval) + else + newval + end + end + end + + def self.fingerprint(source) + Digest::SHA1.base64digest(source).gsub(/[^a-z]/i, '')[0..15] + end + + def self.card_merge(old_param, new_param) + if new_param[:number] ||= old_param[:number] + if new_param[:last4] + new_param[:number] = new_param[:number][0..-5] + new_param[:last4] + else + new_param[:last4] = new_param[:number][-4..-1] + end + end + old_param.merge(new_param) + end + + end +end diff --git a/lib/stripe_mock/version.rb b/lib/stripe_mock/version.rb new file mode 100644 index 0000000..a1fd0ea --- /dev/null +++ b/lib/stripe_mock/version.rb @@ -0,0 +1,4 @@ +module StripeMock + # stripe-ruby-mock version + VERSION = "3.1.0.rc3" +end diff --git a/lib/stripe_mock/webhook_fixtures/account.application.deauthorized.json b/lib/stripe_mock/webhook_fixtures/account.application.deauthorized.json new file mode 100644 index 0000000..0f7b660 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/account.application.deauthorized.json @@ -0,0 +1,12 @@ +{ + "type": "account.application.deauthorized", + "object": "event", + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "data": { + "object": { + "id": "cus_00000000000000" + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/account.external_account.created.json b/lib/stripe_mock/webhook_fixtures/account.external_account.created.json new file mode 100644 index 0000000..b1307db --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/account.external_account.created.json @@ -0,0 +1,27 @@ +{ + "created":1326853478, + "livemode":false, + "id":"evt_00000000000000", + "type":"account.external_account.created", + "object":"event", + "data":{ + "object":{ + "id":"ba_00000000000000", + "object":"bank_account", + "account":"acct_00000000000000", + "account_holder_name":"Jane Austen", + "account_holder_type":"individual", + "bank_name":"STRIPE TEST BANK", + "country":"US", + "currency":"eur", + "default_for_currency":false, + "fingerprint":"efGCBmiwp56O1lsN", + "last4":"6789", + "metadata":{ + + }, + "routing_number":"110000000", + "status":"new" + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/account.external_account.deleted.json b/lib/stripe_mock/webhook_fixtures/account.external_account.deleted.json new file mode 100644 index 0000000..25725d0 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/account.external_account.deleted.json @@ -0,0 +1,27 @@ +{ + "created":1326853478, + "livemode":false, + "id":"evt_00000000000000", + "type":"account.external_account.deleted", + "object":"event", + "data":{ + "object":{ + "id":"ba_00000000000000", + "object":"bank_account", + "account":"acct_00000000000000", + "account_holder_name":"Jane Austen", + "account_holder_type":"individual", + "bank_name":"STRIPE TEST BANK", + "country":"US", + "currency":"eur", + "default_for_currency":false, + "fingerprint":"efGCBmiwp56O1lsN", + "last4":"6789", + "metadata":{ + + }, + "routing_number":"110000000", + "status":"new" + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/account.external_account.updated.json b/lib/stripe_mock/webhook_fixtures/account.external_account.updated.json new file mode 100644 index 0000000..f8932ba --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/account.external_account.updated.json @@ -0,0 +1,27 @@ +{ + "created":1326853478, + "livemode":false, + "id":"evt_00000000000000", + "type":"account.external_account.updated", + "object":"event", + "data":{ + "object":{ + "id":"ba_00000000000000", + "object":"bank_account", + "account":"acct_00000000000000", + "account_holder_name":"Jane Austen", + "account_holder_type":"individual", + "bank_name":"STRIPE TEST BANK", + "country":"US", + "currency":"eur", + "default_for_currency":false, + "fingerprint":"efGCBmiwp56O1lsN", + "last4":"6789", + "metadata":{ + + }, + "routing_number":"110000000", + "status":"new" + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/account.updated.json b/lib/stripe_mock/webhook_fixtures/account.updated.json new file mode 100644 index 0000000..255b8e3 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/account.updated.json @@ -0,0 +1,26 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "account.updated", + "object": "event", + "data": { + "object": { + "id": "acct_00000000000000", + "email": "test@stripe.com", + "statement_descriptor": "TEST", + "details_submitted": true, + "charge_enabled": false, + "payouts_enabled": false, + "currencies_supported": [ + "USD" + ], + "default_currency": "USD", + "country": "US", + "object": "account" + }, + "previous_attributes": { + "details_submitted": false + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/balance.available.json b/lib/stripe_mock/webhook_fixtures/balance.available.json new file mode 100644 index 0000000..60cbe7b --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/balance.available.json @@ -0,0 +1,31 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "balance.available", + "object": "event", + "data": { + "object": { + "pending": [ + { + "amount": 2217, + "currency": "usd" + } + ], + "available": [ + { + "amount": 0, + "currency": "usd" + } + ], + "instant_available": [ + { + "amount": 0, + "currency": "usd" + } + ], + "livemode": false, + "object": "balance" + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/charge.dispute.closed.json b/lib/stripe_mock/webhook_fixtures/charge.dispute.closed.json new file mode 100644 index 0000000..3d6399b --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/charge.dispute.closed.json @@ -0,0 +1,22 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "charge.dispute.closed", + "object": "event", + "data": { + "object": { + "charge": "ch_00000000000000", + "amount": 1000, + "created": 1381080229, + "status": "won", + "livemode": false, + "currency": "usd", + "object": "dispute", + "reason": "general", + "balance_transaction": "txn_00000000000000", + "evidence_due_by": 1382745599, + "evidence": "Here is some evidence" + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/charge.dispute.created.json b/lib/stripe_mock/webhook_fixtures/charge.dispute.created.json new file mode 100644 index 0000000..96de1e5 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/charge.dispute.created.json @@ -0,0 +1,22 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "charge.dispute.created", + "object": "event", + "data": { + "object": { + "charge": "ch_00000000000000", + "amount": 1000, + "created": 1381080223, + "status": "needs_response", + "livemode": false, + "currency": "usd", + "object": "dispute", + "reason": "general", + "balance_transaction": "txn_00000000000000", + "evidence_due_by": 1382745599, + "evidence": null + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/charge.dispute.funds_reinstated.json b/lib/stripe_mock/webhook_fixtures/charge.dispute.funds_reinstated.json new file mode 100644 index 0000000..22411c8 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/charge.dispute.funds_reinstated.json @@ -0,0 +1,88 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "charge.dispute.funds_reinstated", + "object": "event", + "request": null, + "pending_webhooks": 1, + "api_version": "2017-12-14", + "data": { + "object": { + "id": "dp_00000000000000", + "object": "dispute", + "amount": 25000, + "balance_transaction": "txn_00000000000000", + "balance_transactions": [ + { + "id": "txn_1Bl3mMGWCopOTFn18p8iALq8", + "object": "balance_transaction", + "amount": -25000, + "available_on": 1516233600, + "created": 1516145022, + "currency": "usd", + "description": "Chargeback withdrawal for ch_1Bl3mKGWCopOTFn1LKoN557r", + "exchange_rate": null, + "fee": 1500, + "fee_details": [ + { + "amount": 1500, + "application": null, + "currency": "usd", + "description": "Dispute fee", + "type": "stripe_fee" + } + ], + "net": -26500, + "source": "dp_1Bl3mMGWCopOTFn1jpVaJDrU", + "status": "pending", + "type": "adjustment" + } + ], + "charge": "ch_00000000000000", + "created": 1516145022, + "currency": "usd", + "evidence": { + "access_activity_log": null, + "billing_address": null, + "cancellation_policy": null, + "cancellation_policy_disclosure": null, + "cancellation_rebuttal": null, + "customer_communication": null, + "customer_email_address": "amitree.apu@gmail.com", + "customer_name": "amitree.apu@gmail.com", + "customer_purchase_ip": "157.131.133.10", + "customer_signature": null, + "duplicate_charge_documentation": null, + "duplicate_charge_explanation": null, + "duplicate_charge_id": null, + "product_description": null, + "receipt": null, + "refund_policy": null, + "refund_policy_disclosure": null, + "refund_refusal_explanation": null, + "service_date": null, + "service_documentation": null, + "shipping_address": null, + "shipping_carrier": null, + "shipping_date": null, + "shipping_documentation": null, + "shipping_tracking_number": null, + "uncategorized_file": null, + "uncategorized_text": null + }, + "evidence_details": { + "due_by": 1517529599, + "has_evidence": false, + "past_due": false, + "submission_count": 0 + }, + "is_charge_refundable": false, + "livemode": false, + "metadata": { + }, + "reason": "fraudulent", + "status": "needs_response" + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/charge.dispute.funds_withdrawn.json b/lib/stripe_mock/webhook_fixtures/charge.dispute.funds_withdrawn.json new file mode 100644 index 0000000..50210ff --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/charge.dispute.funds_withdrawn.json @@ -0,0 +1,88 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "charge.dispute.funds_withdrawn", + "object": "event", + "request": null, + "pending_webhooks": 1, + "api_version": "2017-12-14", + "data": { + "object": { + "id": "dp_00000000000000", + "object": "dispute", + "amount": 25000, + "balance_transaction": "txn_00000000000000", + "balance_transactions": [ + { + "id": "txn_1Bl3mMGWCopOTFn18p8iALq8", + "object": "balance_transaction", + "amount": -25000, + "available_on": 1516233600, + "created": 1516145022, + "currency": "usd", + "description": "Chargeback withdrawal for ch_1Bl3mKGWCopOTFn1LKoN557r", + "exchange_rate": null, + "fee": 1500, + "fee_details": [ + { + "amount": 1500, + "application": null, + "currency": "usd", + "description": "Dispute fee", + "type": "stripe_fee" + } + ], + "net": -26500, + "source": "dp_1Bl3mMGWCopOTFn1jpVaJDrU", + "status": "pending", + "type": "adjustment" + } + ], + "charge": "ch_00000000000000", + "created": 1516145022, + "currency": "usd", + "evidence": { + "access_activity_log": null, + "billing_address": null, + "cancellation_policy": null, + "cancellation_policy_disclosure": null, + "cancellation_rebuttal": null, + "customer_communication": null, + "customer_email_address": "amitree.apu@gmail.com", + "customer_name": "amitree.apu@gmail.com", + "customer_purchase_ip": "157.131.133.10", + "customer_signature": null, + "duplicate_charge_documentation": null, + "duplicate_charge_explanation": null, + "duplicate_charge_id": null, + "product_description": null, + "receipt": null, + "refund_policy": null, + "refund_policy_disclosure": null, + "refund_refusal_explanation": null, + "service_date": null, + "service_documentation": null, + "shipping_address": null, + "shipping_carrier": null, + "shipping_date": null, + "shipping_documentation": null, + "shipping_tracking_number": null, + "uncategorized_file": null, + "uncategorized_text": null + }, + "evidence_details": { + "due_by": 1517529599, + "has_evidence": false, + "past_due": false, + "submission_count": 0 + }, + "is_charge_refundable": false, + "livemode": false, + "metadata": { + }, + "reason": "fraudulent", + "status": "needs_response" + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/charge.dispute.updated.json b/lib/stripe_mock/webhook_fixtures/charge.dispute.updated.json new file mode 100644 index 0000000..921e830 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/charge.dispute.updated.json @@ -0,0 +1,25 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "charge.dispute.updated", + "object": "event", + "data": { + "object": { + "charge": "ch_00000000000000", + "amount": 1000, + "created": 1381080226, + "status": "under_review", + "livemode": false, + "currency": "usd", + "object": "dispute", + "reason": "general", + "balance_transaction": "txn_00000000000000", + "evidence_due_by": 1382745599, + "evidence": "Here is some evidence" + }, + "previous_attributes": { + "evidence": null + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/charge.failed.json b/lib/stripe_mock/webhook_fixtures/charge.failed.json new file mode 100644 index 0000000..2d55151 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/charge.failed.json @@ -0,0 +1,184 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "charge.failed", + "object": "event", + "request": null, + "pending_webhooks": 1, + "api_version": "2018-02-28", + "data": { + "object": { + "id": "ch_00000000000000", + "object": "charge", + "amount": 100, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_00000000000000", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": "Jenny Rosen", + "phone": null + }, + "captured": false, + "created": 1572389205, + "currency": "cad", + "customer": null, + "description": "My First Test Charge (created for API docs)", + "dispute": null, + "failure_code": null, + "failure_message": null, + "fraud_details": {}, + "invoice": null, + "livemode": false, + "metadata": {}, + "on_behalf_of": null, + "order": null, + "outcome": null, + "paid": false, + "payment_intent": null, + "payment_method": "card_00000000000000", + "payment_method_details": { + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": null + }, + "country": "US", + "exp_month": 8, + "exp_year": 2019, + "fingerprint": "C8aRpBae2T8GeJcn", + "funding": "credit", + "installments": null, + "last4": "4242", + "network": "visa", + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": null, + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/acct_1C0lZEHvOcc4e36o/ch_1FZ3SbHsssvOcc4e36o87NHFK7i/rcpt_G5DuglJFhskXjsEDnLzxF6ESuGQe0Qj", + "refunded": false, + "refunds": { + "object": "list", + "data": [], + "has_more": false, + "url": "/v1/charges/ch_1FZ3SbHvOcc4e36o87NHFK7i/refunds" + }, + "review": null, + "shipping": null, + "source_transfer": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "failed", + "transfer_data": null, + "transfer_group": null + } + }, + "webhook": { + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "charge.failed", + "object": "event", + "request": null, + "pending_webhooks": 1, + "api_version": "2018-02-28", + "data": { + "object": { + "id": "ch_00000000000000", + "object": "charge", + "amount": 100, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_00000000000000", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": "Jenny Rosen", + "phone": null + }, + "captured": false, + "created": 1572389205, + "currency": "cad", + "customer": null, + "description": "My First Test Charge (created for API docs)", + "dispute": null, + "failure_code": null, + "failure_message": null, + "fraud_details": {}, + "invoice": null, + "livemode": false, + "metadata": {}, + "on_behalf_of": null, + "order": null, + "outcome": null, + "paid": false, + "payment_intent": null, + "payment_method": "card_00000000000000", + "payment_method_details": { + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": null + }, + "country": "US", + "exp_month": 8, + "exp_year": 2019, + "fingerprint": "C8aRpBae2T8GeJcn", + "funding": "credit", + "installments": null, + "last4": "4242", + "network": "visa", + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": null, + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/acct_1C0lZEHvOcc4e36o/ch_1FZ3SbHvOcc4sssse36o87NHFK7i/rcpt_G5DuglJFhskXjsEDnLzxF6ESuGQe0Qj", + "refunded": false, + "refunds": { + "object": "list", + "data": [], + "has_more": false, + "url": "/v1/charges/ch_1FZ3SbHvOccssssss4e36o87NHFK7i/refunds" + }, + "review": null, + "shipping": null, + "source_transfer": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "failed", + "transfer_data": null, + "transfer_group": null + } + } + } +} \ No newline at end of file diff --git a/lib/stripe_mock/webhook_fixtures/charge.refunded.json b/lib/stripe_mock/webhook_fixtures/charge.refunded.json new file mode 100644 index 0000000..482d879 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/charge.refunded.json @@ -0,0 +1,69 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "charge.refunded", + "object": "event", + "data": { + "object": { + "id": "ch_00000000000000", + "object": "charge", + "created": 1380933505, + "livemode": false, + "paid": true, + "amount": 1000, + "currency": "usd", + "refunded": true, + "source": { + "id": "cc_00000000000000", + "object": "card", + "last4": "4242", + "type": "Visa", + "brand": "Visa", + "funding": "credit", + "exp_month": 12, + "exp_year": 2013, + "fingerprint": "wXWJT135mEK107G8", + "customer": "cus_00000000000000", + "country": "US", + "name": "Actual Nothing", + "address_line1": null, + "address_line2": null, + "address_city": null, + "address_state": null, + "address_zip": null, + "address_country": null, + "cvc_check": null, + "address_line1_check": null, + "address_zip_check": null + }, + "captured": true, + "refunds": { + "object": "list", + "total_count": 1, + "has_more": false, + "data": [ + { + "amount": 1000, + "currency": "usd", + "created": 1381080103, + "object": "refund", + "balance_transaction": "txn_2hkjgg43ucu7K1", + "id": "re_00000000000000" + } + ] + }, + "balance_transaction": "txn_00000000000000", + "failure_message": null, + "failure_code": null, + "amount_refunded": 1000, + "customer": "cus_00000000000000", + "invoice": "in_00000000000000", + "description": null, + "dispute": null, + "metadata": { + }, + "fee": 0 + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/charge.succeeded.json b/lib/stripe_mock/webhook_fixtures/charge.succeeded.json new file mode 100644 index 0000000..2058b94 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/charge.succeeded.json @@ -0,0 +1,55 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "charge.succeeded", + "object": "event", + "data": { + "object": { + "id": "ch_00000000000000", + "object": "charge", + "created": 1380933505, + "livemode": false, + "paid": true, + "amount": 1000, + "currency": "usd", + "refunded": false, + "source": { + "id": "cc_00000000000000", + "object": "card", + "last4": "4242", + "type": "Visa", + "brand": "Visa", + "exp_month": 12, + "exp_year": 2013, + "fingerprint": "wXWJT135mEK107G8", + "customer": "cus_00000000000000", + "country": "US", + "name": "Actual Nothing", + "address_line1": null, + "address_line2": null, + "address_city": null, + "address_state": null, + "address_zip": null, + "address_country": null, + "cvc_check": null, + "address_line1_check": null, + "address_zip_check": null + }, + "captured": true, + "refunds": { + + }, + "balance_transaction": "txn_00000000000000", + "failure_message": null, + "failure_code": null, + "amount_refunded": 0, + "customer": "cus_00000000000000", + "invoice": "in_00000000000000", + "description": null, + "dispute": null, + "metadata": { + } + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/charge.updated.json b/lib/stripe_mock/webhook_fixtures/charge.updated.json new file mode 100644 index 0000000..707aedd --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/charge.updated.json @@ -0,0 +1,58 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "charge.updated", + "object": "event", + "data": { + "object": { + "id": "ch_00000000000000", + "object": "charge", + "created": 1380933505, + "livemode": false, + "paid": true, + "amount": 1000, + "currency": "usd", + "refunded": false, + "source": { + "id": "cc_00000000000000", + "object": "card", + "last4": "4242", + "type": "Visa", + "brand": "Visa", + "exp_month": 12, + "exp_year": 2013, + "fingerprint": "wXWJT135mEK107G8", + "customer": "cus_00000000000000", + "country": "US", + "name": "Actual Nothing", + "address_line1": null, + "address_line2": null, + "address_city": null, + "address_state": null, + "address_zip": null, + "address_country": null, + "cvc_check": null, + "address_line1_check": null, + "address_zip_check": null + }, + "captured": true, + "refunds": { + + }, + "balance_transaction": "txn_00000000000000", + "failure_message": null, + "failure_code": null, + "amount_refunded": 0, + "customer": "cus_00000000000000", + "invoice": "in_00000000000000", + "description": "Sample description" , + "dispute": null, + "metadata": { + } + }, + "previous_attributes": { + "description": null + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/checkout.session.completed.payment_mode.json b/lib/stripe_mock/webhook_fixtures/checkout.session.completed.payment_mode.json new file mode 100644 index 0000000..e7531dc --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/checkout.session.completed.payment_mode.json @@ -0,0 +1,53 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "checkout.session.completed", + "object": "event", + "data": { + "object": { + "id": "cs_00000000000000", + "object": "checkout.session", + "allow_promotion_codes": null, + "amount_subtotal": 25000, + "amount_total": 25000, + "automatic_tax": { + "enabled": false, + "status": null + }, + "billing_address_collection": null, + "cancel_url": "https://example.com/cancel", + "client_reference_id": null, + "currency": "usd", + "customer": "cus_00000000000000", + "customer_details": { + "email": "example@example.com", + "tax_exempt": "none", + "tax_ids": [] + }, + "customer_email": null, + "livemode": false, + "locale": null, + "metadata": {}, + "mode": "payment", + "payment_intent": "pi_00000000000000", + "payment_method_options": {}, + "payment_method_types": [ + "card" + ], + "payment_status": "paid", + "setup_intent": null, + "shipping": null, + "shipping_address_collection": null, + "submit_type": null, + "subscription": null, + "success_url": "https://example.com/success", + "total_details": { + "amount_discount": 0, + "amount_shipping": 0, + "amount_tax": 0 + }, + "url": null + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/checkout.session.completed.setup_mode.json b/lib/stripe_mock/webhook_fixtures/checkout.session.completed.setup_mode.json new file mode 100644 index 0000000..3be9b68 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/checkout.session.completed.setup_mode.json @@ -0,0 +1,45 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "checkout.session.completed", + "object": "event", + "data": { + "object": { + "id": "cs_00000000000000", + "object": "checkout.session", + "allow_promotion_codes": null, + "amount_subtotal": null, + "amount_total": null, + "automatic_tax": { + "enabled": false, + "status": null + }, + "billing_address_collection": null, + "cancel_url": "https://example.com/cancel", + "client_reference_id": null, + "currency": null, + "customer": null, + "customer_details": null, + "customer_email": null, + "livemode": false, + "locale": null, + "metadata": {}, + "mode": "setup", + "payment_intent": null, + "payment_method_options": {}, + "payment_method_types": [ + "card" + ], + "payment_status": "no_payment_required", + "setup_intent": "seti_00000000000000", + "shipping": null, + "shipping_address_collection": null, + "submit_type": null, + "subscription": null, + "success_url": "https://example.com/success", + "total_details": null, + "url": null + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/coupon.created.json b/lib/stripe_mock/webhook_fixtures/coupon.created.json new file mode 100644 index 0000000..3f128c3 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/coupon.created.json @@ -0,0 +1,23 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "coupon.created", + "object": "event", + "data": { + "object": { + "id": "25OFF_00000000000000", + "percent_off": 25, + "amount_off": null, + "currency": "usd", + "object": "coupon", + "livemode": false, + "duration": "repeating", + "redeem_by": null, + "max_redemptions": null, + "times_redeemed": 0, + "duration_in_months": 3, + "valid": true + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/coupon.deleted.json b/lib/stripe_mock/webhook_fixtures/coupon.deleted.json new file mode 100644 index 0000000..656adb8 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/coupon.deleted.json @@ -0,0 +1,23 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "coupon.deleted", + "object": "event", + "data": { + "object": { + "id": "25OFF_00000000000000", + "percent_off": 25, + "amount_off": null, + "currency": "usd", + "object": "coupon", + "livemode": false, + "duration": "repeating", + "redeem_by": null, + "max_redemptions": null, + "times_redeemed": 0, + "duration_in_months": 3, + "valid": false + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/customer.created.json b/lib/stripe_mock/webhook_fixtures/customer.created.json new file mode 100644 index 0000000..7d6569a --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/customer.created.json @@ -0,0 +1,55 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "customer.created", + "object": "event", + "data": { + "object": { + "object": "customer", + "created": 1375148334, + "id": "cus_00000000000000", + "livemode": false, + "description": null, + "email": "bond@mailinator.com", + "delinquent": true, + "metadata": { + }, + "preferred_locales": [], + "subscription": null, + "discount": null, + "account_balance": 0, + "sources": { + "object": "list", + "count": 1, + "url": "/v1/customers/cus_2I2AhGQOPmEFeu/cards", + "data": [ + { + "id": "cc_2I2akIhmladin5", + "object": "card", + "last4": "0341", + "type": "Visa", + "brand": "Visa", + "funding": "credit", + "exp_month": 12, + "exp_year": 2013, + "fingerprint": "fWvZEzdbEIFF8QrK", + "customer": "cus_2I2AhGQOPmEFeu", + "country": "US", + "name": "Johnny Goodman", + "address_line1": null, + "address_line2": null, + "address_city": null, + "address_state": null, + "address_zip": null, + "address_country": null, + "cvc_check": "pass", + "address_line1_check": null, + "address_zip_check": null + } + ] + }, + "default_card": "cc_2I2akIhmladin5" + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/customer.deleted.json b/lib/stripe_mock/webhook_fixtures/customer.deleted.json new file mode 100644 index 0000000..70e6560 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/customer.deleted.json @@ -0,0 +1,42 @@ +{ + "created": 1326853478, + "data": { + "object": { + "account_balance": 0, + "active_card": { + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "country": "US", + "cvc_check": "pass", + "exp_month": 12, + "exp_year": 2013, + "fingerprint": "wXWJT135mEK107G8", + "last4": "4242", + "name": "1231", + "object": "card", + "type": "Visa", + "brand": "Visa", + "funding": "credit" + }, + "created": 1359947599, + "delinquent": false, + "description": null, + "discount": null, + "email": "ajoe@mailinator.com", + "id": "cus_00000000000000", + "livemode": false, + "object": "customer", + "subscription": null + } + }, + "id": "evt_00000000000000", + "livemode": false, + "object": "event", + "type": "customer.deleted" +} diff --git a/lib/stripe_mock/webhook_fixtures/customer.discount.created.json b/lib/stripe_mock/webhook_fixtures/customer.discount.created.json new file mode 100644 index 0000000..0edf201 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/customer.discount.created.json @@ -0,0 +1,28 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "customer.discount.created", + "object": "event", + "data": { + "object": { + "coupon": { + "id": "25OFF_00000000000000", + "percent_off": 25, + "amount_off": null, + "currency": "usd", + "object": "coupon", + "livemode": false, + "duration": "repeating", + "redeem_by": null, + "max_redemptions": null, + "times_redeemed": 0, + "duration_in_months": 3 + }, + "start": 1381080505, + "object": "discount", + "customer": "cus_00000000000000", + "end": 1389029305 + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/customer.discount.deleted.json b/lib/stripe_mock/webhook_fixtures/customer.discount.deleted.json new file mode 100644 index 0000000..8c26dfe --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/customer.discount.deleted.json @@ -0,0 +1,28 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "customer.discount.deleted", + "object": "event", + "data": { + "object": { + "coupon": { + "id": "25OFF_00000000000000", + "percent_off": 25, + "amount_off": null, + "currency": "usd", + "object": "coupon", + "livemode": false, + "duration": "repeating", + "redeem_by": null, + "max_redemptions": null, + "times_redeemed": 0, + "duration_in_months": 3 + }, + "start": 1381080512, + "object": "discount", + "customer": "cus_00000000000000", + "end": 1389029312 + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/customer.discount.updated.json b/lib/stripe_mock/webhook_fixtures/customer.discount.updated.json new file mode 100644 index 0000000..ff448ee --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/customer.discount.updated.json @@ -0,0 +1,43 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "customer.discount.updated", + "object": "event", + "data": { + "object": { + "coupon": { + "id": "25OFF_00000000000000", + "percent_off": 25, + "amount_off": null, + "currency": "usd", + "object": "coupon", + "livemode": false, + "duration": "repeating", + "redeem_by": null, + "max_redemptions": null, + "times_redeemed": 0, + "duration_in_months": 3 + }, + "start": 1381080509, + "object": "discount", + "customer": "cus_00000000000000", + "end": 1389029309 + }, + "previous_attributes": { + "coupon": { + "id": "OLD_COUPON_ID", + "percent_off": 25, + "amount_off": null, + "currency": "usd", + "object": "coupon", + "livemode": false, + "duration": "repeating", + "redeem_by": null, + "max_redemptions": null, + "times_redeemed": 0, + "duration_in_months": 3 + } + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/customer.source.created.json b/lib/stripe_mock/webhook_fixtures/customer.source.created.json new file mode 100644 index 0000000..91eba12 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/customer.source.created.json @@ -0,0 +1,32 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "customer.source.created", + "object": "event", + "data": { + "object": { + "id": "card_VALID", + "object": "card", + "last4": "4242", + "type": "Visa", + "brand": "Visa", + "funding": "credit", + "exp_month": 3, + "exp_year": 2020, + "fingerprint": "wXWJT135mEK107G8", + "customer": "cus_VALID", + "country": "US", + "name": "Testy Tester", + "address_line1": null, + "address_line2": null, + "address_city": null, + "address_state": null, + "address_zip": null, + "address_country": null, + "cvc_check": "pass", + "address_line1_check": null, + "address_zip_check": null + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/customer.source.deleted.json b/lib/stripe_mock/webhook_fixtures/customer.source.deleted.json new file mode 100644 index 0000000..665e26f --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/customer.source.deleted.json @@ -0,0 +1,32 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "customer.source.deleted", + "object": "event", + "data": { + "object": { + "id": "card_VALID", + "object": "card", + "last4": "4242", + "type": "Visa", + "brand": "Visa", + "funding": "credit", + "exp_month": 3, + "exp_year": 2020, + "fingerprint": "wXWJT135mEK107G8", + "customer": "cus_VALID", + "country": "US", + "name": "Testy Tester", + "address_line1": null, + "address_line2": null, + "address_city": null, + "address_state": null, + "address_zip": null, + "address_country": null, + "cvc_check": "pass", + "address_line1_check": null, + "address_zip_check": null + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/customer.source.updated.json b/lib/stripe_mock/webhook_fixtures/customer.source.updated.json new file mode 100644 index 0000000..2634deb --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/customer.source.updated.json @@ -0,0 +1,36 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "customer.source.updated", + "object": "event", + "data": { + "object": { + "id": "card_VALID", + "object": "card", + "last4": "4242", + "type": "Visa", + "brand": "Visa", + "funding": "credit", + "exp_month": 3, + "exp_year": 2020, + "fingerprint": "wXWJT135mEK107G8", + "customer": "cus_VALID", + "country": "US", + "name": "Testy Tester", + "address_line1": null, + "address_line2": null, + "address_city": null, + "address_state": null, + "address_zip": null, + "address_country": null, + "cvc_check": "pass", + "address_line1_check": null, + "address_zip_check": null + }, + "previous_attributes": + { + "name": "Testy Tester Jr." + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/customer.subscription.created.json b/lib/stripe_mock/webhook_fixtures/customer.subscription.created.json new file mode 100644 index 0000000..7218cbb --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/customer.subscription.created.json @@ -0,0 +1,66 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "customer.subscription.created", + "object": "event", + "data": { + "object": { + "id": "su_00000000000000", + "items": { + "object": "list", + "data": [ + { + "id": "si_00000000000000", + "object": "subscription_item", + "created": 1497881783, + "plan": { + "interval": "month", + "product": "pr_00000000000000", + "amount": 100, + "currency": "usd", + "id": "fkx0AFo_00000000000000", + "object": "plan", + "livemode": false, + "interval_count": 1, + "trial_period_days": null, + "metadata": {} + }, + "quantity": 1 + }, + { + "id": "si_00000000000001", + "object": "subscription_item", + "created": 1497881788, + "plan": { + "interval": "month", + "product": "pr_00000000000001", + "amount": 200, + "currency": "eur", + "id": "fkx0AFo_00000000000001", + "object": "plan", + "livemode": false, + "interval_count": 1, + "trial_period_days": null, + "metadata": {} + }, + "quantity": 5 + } + ] + }, + "object": "subscription", + "start": 1381080557, + "status": "active", + "customer": "cus_00000000000000", + "cancel_at_period_end": false, + "current_period_start": 1381080557, + "current_period_end": 1383758957, + "ended_at": null, + "trial_start": null, + "trial_end": null, + "canceled_at": null, + "quantity": 1, + "application_fee_percent": null + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/customer.subscription.deleted.json b/lib/stripe_mock/webhook_fixtures/customer.subscription.deleted.json new file mode 100644 index 0000000..c9c3ebe --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/customer.subscription.deleted.json @@ -0,0 +1,65 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "customer.subscription.deleted", + "object": "event", + "data": { + "object": { + "id": "su_00000000000000", + "items": { + "object": "list", + "data": [{ + "id": "si_00000000000000", + "object": "subscription_item", + "created": 1497881783, + "plan": { + "interval": "month", + "product": "pr_00000000000000", + "amount": 100, + "currency": "usd", + "id": "fkx0AFo_00000000000000", + "object": "plan", + "livemode": false, + "interval_count": 1, + "trial_period_days": null, + "metadata": {} + }, + "quantity": 1 + }, + { + "id": "si_00000000000001", + "object": "subscription_item", + "created": 1497881788, + "plan": { + "interval": "month", + "product": "pr_00000000000001", + "amount": 200, + "currency": "eur", + "id": "fkx0AFo_00000000000001", + "object": "plan", + "livemode": false, + "interval_count": 1, + "trial_period_days": null, + "metadata": {} + }, + "quantity": 5 + } + ] + }, + "object": "subscription", + "start": 1381080564, + "status": "canceled", + "customer": "cus_00000000000000", + "cancel_at_period_end": false, + "current_period_start": 1381080564, + "current_period_end": 1383758964, + "ended_at": 1381021514, + "trial_start": null, + "trial_end": null, + "canceled_at": null, + "quantity": 1, + "application_fee_percent": null + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/customer.subscription.trial_will_end.json b/lib/stripe_mock/webhook_fixtures/customer.subscription.trial_will_end.json new file mode 100644 index 0000000..5261cd9 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/customer.subscription.trial_will_end.json @@ -0,0 +1,65 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "customer.subscription.trial_will_end", + "object": "event", + "data": { + "object": { + "id": "su_00000000000000", + "items": { + "object": "list", + "data": [{ + "id": "si_00000000000000", + "object": "subscription_item", + "created": 1497881783, + "plan": { + "interval": "month", + "product": "pr_00000000000000", + "amount": 100, + "currency": "usd", + "id": "fkx0AFo_00000000000000", + "object": "plan", + "livemode": false, + "interval_count": 1, + "trial_period_days": null, + "metadata": {} + }, + "quantity": 1 + }, + { + "id": "si_00000000000001", + "object": "subscription_item", + "created": 1497881788, + "plan": { + "interval": "month", + "product": "pr_00000000000001", + "amount": 200, + "currency": "eur", + "id": "fkx0AFo_00000000000001", + "object": "plan", + "livemode": false, + "interval_count": 1, + "trial_period_days": null, + "metadata": {} + }, + "quantity": 5 + } + ] + }, + "object": "subscription", + "start": 1381080623, + "status": "trialing", + "customer": "cus_00000000000000", + "cancel_at_period_end": false, + "current_period_start": 1381080623, + "current_period_end": 1383759023, + "ended_at": null, + "trial_start": 1381021530, + "trial_end": 1381280730, + "canceled_at": null, + "quantity": 1, + "application_fee_percent": null + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/customer.subscription.updated.json b/lib/stripe_mock/webhook_fixtures/customer.subscription.updated.json new file mode 100644 index 0000000..2ca10fe --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/customer.subscription.updated.json @@ -0,0 +1,78 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "customer.subscription.updated", + "object": "event", + "data": { + "object": { + "id": "su_00000000000000", + "items": { + "object": "list", + "data": [{ + "id": "si_00000000000000", + "object": "subscription_item", + "created": 1497881783, + "plan": { + "interval": "month", + "product": "pr_00000000000000", + "amount": 100, + "currency": "usd", + "id": "fkx0AFo_00000000000000", + "object": "plan", + "livemode": false, + "interval_count": 1, + "trial_period_days": null, + "metadata": {} + }, + "quantity": 1 + }, + { + "id": "si_00000000000001", + "object": "subscription_item", + "created": 1497881788, + "plan": { + "interval": "month", + "product": "pr_00000000000001", + "amount": 200, + "currency": "eur", + "id": "fkx0AFo_00000000000001", + "object": "plan", + "livemode": false, + "interval_count": 1, + "trial_period_days": null, + "metadata": {} + }, + "quantity": 5 + } + ] + }, + "object": "subscription", + "start": 1381080561, + "status": "active", + "customer": "cus_00000000000000", + "cancel_at_period_end": false, + "current_period_start": 1381080561, + "current_period_end": 1383758961, + "ended_at": null, + "trial_start": null, + "trial_end": null, + "canceled_at": null, + "quantity": 1, + "application_fee_percent": null + }, + "previous_attributes": { + "plan": { + "interval": "month", + "product": "pr_00000000000002", + "amount": 100, + "currency": "usd", + "id": "OLD_PLAN_ID", + "object": "plan", + "livemode": false, + "interval_count": 1, + "trial_period_days": null + } + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/customer.updated.json b/lib/stripe_mock/webhook_fixtures/customer.updated.json new file mode 100644 index 0000000..afffec5 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/customer.updated.json @@ -0,0 +1,58 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "customer.updated", + "object": "event", + "data": { + "object": { + "object": "customer", + "created": 1375148334, + "id": "cus_00000000000000", + "livemode": false, + "description": null, + "email": "bond@mailinator.com", + "delinquent": true, + "metadata": { + }, + "preferred_locales": [], + "subscription": null, + "discount": null, + "account_balance": 0, + "sources": { + "object": "list", + "count": 1, + "url": "/v1/customers/cus_2I2AhGQOPmEFeu/cards", + "data": [ + { + "id": "cc_2I2akIhmladin5", + "object": "card", + "last4": "0341", + "type": "Visa", + "brand": "Visa", + "funding": "credit", + "exp_month": 12, + "exp_year": 2013, + "fingerprint": "fWvZEzdbEIFF8QrK", + "customer": "cus_2I2AhGQOPmEFeu", + "country": "US", + "name": "Johnny Goodman", + "address_line1": null, + "address_line2": null, + "address_city": null, + "address_state": null, + "address_zip": null, + "address_country": null, + "cvc_check": "pass", + "address_line1_check": null, + "address_zip_check": null + } + ] + }, + "default_source": "cc_2I2akIhmladin5" + }, + "previous_attributes": { + "description": "Old description" + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/invoice.created.json b/lib/stripe_mock/webhook_fixtures/invoice.created.json new file mode 100644 index 0000000..c4aa066 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/invoice.created.json @@ -0,0 +1,71 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "invoice.created", + "status": "paid", + "object": "event", + "data": { + "object": { + "created": 1380674206, + "id": "in_00000000000000", + "period_start": 1378082075, + "period_end": 1380674075, + "lines": { + "count": 1, + "object": "list", + "url": "/v1/invoices/in_00000000000000/lines", + "data": [ + { + "id": "su_2hksGtIPylSBg2", + "object": "line_item", + "type": "subscription", + "livemode": true, + "amount": 100, + "currency": "usd", + "proration": false, + "period": { + "start": 1383759042, + "end": 1386351042 + }, + "quantity": 1, + "plan": { + "interval": "month", + "amount": 100, + "currency": "usd", + "id": "fkx0AFo", + "object": "plan", + "livemode": false, + "interval_count": 1, + "trial_period_days": null, + "product": "pr_00000000000000", + "metadata": {} + }, + "description": null, + "metadata": null + } + ] + }, + "subtotal": 1000, + "total": 1000, + "customer": "cus_00000000000000", + "object": "invoice", + "attempted": false, + "closed": true, + "paid": true, + "livemode": false, + "attempt_count": 1, + "amount_due": 1000, + "currency": "usd", + "starting_balance": 0, + "ending_balance": 0, + "next_payment_attempt": null, + "charge": "ch_00000000000000", + "discount": null, + "application_fee": null, + "subscription": "sub_00000000000000", + "metadata": {}, + "description": null + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/invoice.payment_failed.json b/lib/stripe_mock/webhook_fixtures/invoice.payment_failed.json new file mode 100644 index 0000000..294270e --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/invoice.payment_failed.json @@ -0,0 +1,105 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "invoice.payment_failed", + "object": "event", + "data": { + "object": { + "date": 1380674206, + "id": "in_00000000000000", + "period_start": 1378082075, + "period_end": 1380674075, + "lines": { + "object": "list", + "count": 3, + "url": "/v1/invoices/in_00000000000000/lines", + "data": [ + { + "id": "ii_00000000000000", + "object": "line_item", + "type": "invoiceitem", + "livemode": false, + "amount": 19000, + "currency": "usd", + "proration": true, + "period": { + "start": 1393765661, + "end": 1393765661 + }, + "quantity": null, + "plan": null, + "description": "Remaining time on Platinum after 02 Mar 2014", + "metadata": {} + }, + { + "id": "ii_00000000000001", + "object": "line_item", + "type": "invoiceitem", + "livemode": false, + "amount": -9000, + "currency": "usd", + "proration": true, + "period": { + "start": 1393765661, + "end": 1393765661 + }, + "quantity": null, + "plan": null, + "description": "Unused time on Gold after 05 Mar 2014", + "metadata": {} + }, + { + "id": "su_00000000000000", + "object": "line_item", + "type": "subscription", + "livemode": false, + "amount": 20000, + "currency": "usd", + "proration": false, + "period": { + "start": 1383759053, + "end": 1386351053 + }, + "quantity": 1, + "plan": { + "interval": "month", + "product": "pr_00000000000000", + "created": 1300000000, + "amount": 20000, + "currency": "usd", + "id": "platinum", + "object": "plan", + "livemode": false, + "interval_count": 1, + "trial_period_days": null, + "metadata": {} + }, + "description": null, + "metadata": null + } + ] + }, + "subtotal": 30000, + "total": 30000, + "customer": "cus_00000000000000", + "object": "invoice", + "attempted": true, + "closed": false, + "paid": false, + "livemode": false, + "attempt_count": 1, + "amount_due": 30000, + "currency": "usd", + "starting_balance": 0, + "ending_balance": 0, + "next_payment_attempt": 1380760475, + "charge": "ch_00000000000000", + "discount": null, + "application_fee": null, + "subscription": "su_00000000000000", + "metadata": {}, + "description": null + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/invoice.payment_succeeded.json b/lib/stripe_mock/webhook_fixtures/invoice.payment_succeeded.json new file mode 100644 index 0000000..9be3043 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/invoice.payment_succeeded.json @@ -0,0 +1,112 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "invoice.payment_succeeded", + "object": "event", + "data": { + "object": { + "id": "in_00000000000000", + "object": "invoice", + "amount_due": 999, + "application_fee": null, + "attempt_count": 1, + "attempted": true, + "charge": "ch_18EcOcLrgDIZ7iq8TaNlErVv", + "closed": true, + "currency": "eur", + "customer": "cus_00000000000000", + "created": 1464084258, + "description": null, + "discount": null, + "ending_balance": 0, + "forgiven": false, + "lines": { + "data": [{ + "id": "sub_00000000000000", + "object": "line_item", + "amount": 50, + "currency": "eur", + "description": null, + "discountable": true, + "livemode": true, + "metadata": {}, + "period": { + "start": 1500637196, + "end": 1532173196 + }, + "plan": { + "id": "platinum", + "object": "plan", + "amount": 500, + "created": 1499943145, + "currency": "eur", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "name": "New Plan Test", + "statement_descriptor": null, + "trial_period_days": null + }, + "proration": false, + "quantity": 1, + "subscription": null, + "subscription_item": "si_18ZfWyLrgDIZ7iq8fSlSNGIV", + "type": "subscription" + }, + { + "id": "sub_00000000000000", + "object": "line_item", + "amount": 50, + "currency": "eur", + "description": null, + "discountable": true, + "livemode": true, + "metadata": {}, + "period": { + "start": 1500637196, + "end": 1532173196 + }, + "plan": { + "id": "gold", + "object": "plan", + "amount": 300, + "created": 1499943155, + "currency": "eur", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "name": "New gold Plan Test", + "statement_descriptor": null, + "trial_period_days": null + }, + "proration": false, + "quantity": 1, + "subscription": null, + "subscription_item": "si_18ZfWyLrgDIZ7iq8fSlSNGIV", + "type": "subscription" + }], + "total_count": 1, + "object": "list", + "url": "/v1/invoices/in_18EcOcLrgDIZ7iq8zsDkunZ0/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "paid": true, + "period_end": 1464084258, + "period_start": 1464084258, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": null, + "subscription": "sub_00000000000000", + "subtotal": 999, + "tax": null, + "tax_percent": null, + "total": 999, + "webhooks_delivered_at": 1464084258 + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/invoice.upcoming.json b/lib/stripe_mock/webhook_fixtures/invoice.upcoming.json new file mode 100644 index 0000000..72b1860 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/invoice.upcoming.json @@ -0,0 +1,70 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "invoice.upcoming", + "status": "draft", + "object": "event", + "data": { + "object": { + "created": 1380674206, + "period_start": 1378082075, + "period_end": 1380674075, + "lines": { + "count": 1, + "object": "list", + "url": "/v1/invoices/upcoming/lines?customer=cus_00000000000000", + "data": [ + { + "id": "su_2hksGtIPylSBg2", + "object": "line_item", + "type": "subscription", + "livemode": true, + "amount": 100, + "currency": "usd", + "proration": false, + "period": { + "start": 1383759042, + "end": 1386351042 + }, + "quantity": 1, + "plan": { + "interval": "month", + "amount": 100, + "currency": "usd", + "id": "fkx0AFo", + "object": "plan", + "livemode": false, + "interval_count": 1, + "trial_period_days": null, + "product": "pr_00000000000000", + "metadata": {} + }, + "description": null, + "metadata": null + } + ] + }, + "subtotal": 1000, + "total": 1000, + "customer": "cus_00000000000000", + "object": "invoice", + "attempted": false, + "closed": false, + "paid": false, + "livemode": false, + "attempt_count": 0, + "amount_due": 1000, + "currency": "usd", + "starting_balance": 0, + "ending_balance": 0, + "next_payment_attempt": null, + "charge": null, + "discount": null, + "application_fee": null, + "subscription": "sub_00000000000000", + "metadata": {}, + "description": null + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/invoice.updated.json b/lib/stripe_mock/webhook_fixtures/invoice.updated.json new file mode 100644 index 0000000..e27be0d --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/invoice.updated.json @@ -0,0 +1,74 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "invoice.updated", + "status": "paid", + "object": "event", + "data": { + "object": { + "created": 1380674206, + "id": "in_00000000000000", + "period_start": 1378082075, + "period_end": 1380674075, + "lines": { + "count": 1, + "object": "list", + "url": "/v1/invoices/in_00000000000000/lines", + "data": [ + { + "id": "su_00000000000000", + "object": "line_item", + "type": "subscription", + "livemode": true, + "amount": 100, + "currency": "usd", + "proration": false, + "period": { + "start": 1383759047, + "end": 1386351047 + }, + "quantity": 1, + "plan": { + "interval": "month", + "product": "pr_00000000000000", + "amount": 100, + "currency": "usd", + "id": "fkx0AFo", + "object": "plan", + "livemode": false, + "interval_count": 1, + "trial_period_days": null, + "metadata": {} + }, + "description": null, + "metadata": null + } + ] + }, + "subtotal": 1000, + "total": 1000, + "customer": "cus_00000000000000", + "object": "invoice", + "attempted": true, + "closed": true, + "paid": true, + "livemode": false, + "attempt_count": 1, + "amount_due": 1000, + "currency": "usd", + "starting_balance": 0, + "ending_balance": 0, + "next_payment_attempt": null, + "charge": "ch_00000000000000", + "discount": null, + "application_fee": null, + "subscription": "sub_00000000000000", + "metadata": {}, + "description": null + }, + "previous_attributes": { + "lines": [] + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/invoiceitem.created.json b/lib/stripe_mock/webhook_fixtures/invoiceitem.created.json new file mode 100644 index 0000000..a51ce88 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/invoiceitem.created.json @@ -0,0 +1,21 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "invoiceitem.created", + "object": "event", + "data": { + "object": { + "object": "invoiceitem", + "id": "ii_00000000000000", + "date": 1372126711, + "amount": 2500, + "livemode": false, + "proration": false, + "currency": "usd", + "customer": "cus_00000000000000", + "description": "Plan: Veteran's Club Signup Fee Plan Id: 4", + "invoice": "in_00000000000000" + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/invoiceitem.deleted.json b/lib/stripe_mock/webhook_fixtures/invoiceitem.deleted.json new file mode 100644 index 0000000..b566ca8 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/invoiceitem.deleted.json @@ -0,0 +1,21 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "invoiceitem.deleted", + "object": "event", + "data": { + "object": { + "object": "invoiceitem", + "id": "ii_00000000000000", + "date": 1372126711, + "amount": 2500, + "livemode": false, + "proration": false, + "currency": "usd", + "customer": "cus_00000000000000", + "description": "Plan: Veteran's Club Signup Fee Plan Id: 4", + "invoice": "in_00000000000000" + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/invoiceitem.updated.json b/lib/stripe_mock/webhook_fixtures/invoiceitem.updated.json new file mode 100644 index 0000000..c049c8d --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/invoiceitem.updated.json @@ -0,0 +1,24 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "invoiceitem.updated", + "object": "event", + "data": { + "object": { + "object": "invoiceitem", + "id": "ii_00000000000000", + "date": 1372126711, + "amount": 2500, + "livemode": false, + "proration": false, + "currency": "usd", + "customer": "cus_00000000000000", + "description": "Plan: Veteran's Club Signup Fee Plan Id: 4", + "invoice": "in_00000000000000" + }, + "previous_attributes": { + "amount": 2121 + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/mandate.updated.json b/lib/stripe_mock/webhook_fixtures/mandate.updated.json new file mode 100644 index 0000000..858fe36 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/mandate.updated.json @@ -0,0 +1,34 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "mandate.updated", + "object": "event", + "data": { + "object": { + "id": "mandate_000000000000000000000000", + "object": "mandate", + "customer_acceptance": { + "accepted_at": 1326853478, + "online": { + "ip_address": "0.0.0.0", + "user_agent": "UserAgent" + }, + "type": "online" + }, + "livemode": false, + "multi_use": {}, + "payment_method": "pm_000000000000000000000000", + "payment_method_details": {}, + "status": "active", + "type": "multi_use" + }, + "previous_attributes": { + "payment_method_details": { + "bacs_debit": { + "network_status": "pending" + } + } + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/payment_intent.canceled.json b/lib/stripe_mock/webhook_fixtures/payment_intent.canceled.json new file mode 100644 index 0000000..46f5142 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/payment_intent.canceled.json @@ -0,0 +1,68 @@ +{ + "id": "evt_00000000000000", + "object": "event", + "api_version": "2018-02-28", + "created": 1578499109, + "data": { + "object": { + "id": "pi_00000000000000", + "object": "payment_intent", + "amount": 900, + "amount_capturable": 0, + "amount_received": 0, + "application": null, + "application_fee_amount": null, + "canceled_at": 1578499109, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/charges?payment_intent=pi_00000000000000" + }, + "client_secret": "pi_00000000000000", + "confirmation_method": "automatic", + "created": 1578499108, + "currency": "eur", + "customer": "cus_00000000000000", + "description": null, + "invoice": null, + "last_payment_error": null, + "livemode": false, + "metadata": {}, + "next_action": null, + "on_behalf_of": null, + "payment_method": "pm_00000000000000", + "payment_method_options": { + "card": { + "installments": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card", + "sepa_debit" + ], + "receipt_email": null, + "review": null, + "setup_future_usage": null, + "shipping": null, + "source": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "canceled", + "transfer_data": null, + "transfer_group": null + } + }, + "livemode": false, + "pending_webhooks": 2, + "request": { + "id": "req_00000000000000", + "idempotency_key": null + }, + "type": "payment_intent.canceled" +} diff --git a/lib/stripe_mock/webhook_fixtures/payment_intent.payment_failed.json b/lib/stripe_mock/webhook_fixtures/payment_intent.payment_failed.json new file mode 100644 index 0000000..cd10f1e --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/payment_intent.payment_failed.json @@ -0,0 +1,186 @@ +{ + "id": "evt_00000000000000", + "object": "event", + "api_version": "2018-02-28", + "created": 1578401135, + "data": { + "object": { + "id": "pi_00000000000000", + "object": "payment_intent", + "allowed_source_types": ["card", "sepa_debit"], + "amount": 200, + "amount_capturable": 0, + "amount_received": 0, + "application": null, + "application_fee_amount": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": { + "object": "list", + "data": [ + { + "id": "py_00000000000000", + "object": "charge", + "amount": 200, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": null, + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": "john.doe@example.com", + "name": "John Doe", + "phone": null + }, + "captured": true, + "created": 1578401129, + "currency": "eur", + "customer": "cus_00000000000000", + "description": null, + "destination": "acct_00000000000000", + "dispute": null, + "disputed": false, + "failure_code": null, + "failure_message": null, + "fraud_details": {}, + "invoice": null, + "livemode": false, + "metadata": {}, + "on_behalf_of": null, + "order": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "not_assessed", + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": false, + "payment_intent": "pi_00000000000000", + "payment_method": "pm_00000000000000", + "payment_method_details": { + "sepa_debit": { + "bank_code": "37040044", + "branch_code": null, + "country": "DE", + "fingerprint": "00000000000000", + "last4": "3001", + "mandate": "mandate_00000000000000" + }, + "type": "sepa_debit" + }, + "receipt_email": null, + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/acct_00000000000000/py_00000000000000/rcpt_00000000000000", + "refunded": false, + "refunds": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/charges/py_00000000000000/refunds" + }, + "review": null, + "shipping": null, + "source": null, + "source_transfer": null, + "statement_descriptor": "ACME Corp", + "statement_descriptor_suffix": null, + "status": "failed", + "transfer_data": { + "amount": null, + "destination": "acct_00000000000000" + }, + "transfer_group": "group_pi_00000000000000" + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/charges?payment_intent=pi_00000000000000" + }, + "client_secret": "pi_00000000000000", + "confirmation_method": "automatic", + "created": 1578401129, + "currency": "eur", + "customer": "cus_00000000000000", + "description": null, + "invoice": null, + "last_payment_error": { + "code": "payment_intent_payment_attempt_failed", + "doc_url": "https://stripe.com/docs/error-codes/payment-intent-payment-attempt-failed", + "message": "The payment failed.", + "payment_method": { + "id": "pm_00000000000000", + "object": "payment_method", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": "john.doe@example.com", + "name": "John Doe", + "phone": null + }, + "created": 1578400666, + "customer": "cus_00000000000000", + "livemode": false, + "metadata": {}, + "sepa_debit": { + "bank_code": "37040044", + "branch_code": "", + "country": "DE", + "fingerprint": "00000000000000", + "last4": "3001" + }, + "type": "sepa_debit" + }, + "type": "invalid_request_error" + }, + "livemode": false, + "metadata": {}, + "next_action": null, + "next_source_action": null, + "on_behalf_of": null, + "payment_method": null, + "payment_method_options": { + "card": { + "installments": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": ["card", "sepa_debit"], + "receipt_email": null, + "review": null, + "setup_future_usage": null, + "shipping": null, + "source": null, + "statement_descriptor": "ACME Corp", + "statement_descriptor_suffix": null, + "status": "requires_source", + "transfer_data": { + "destination": "acct_00000000000000" + }, + "transfer_group": null + } + }, + "livemode": false, + "pending_webhooks": 3, + "request": { + "id": null, + "idempotency_key": null + }, + "type": "payment_intent.payment_failed" +} diff --git a/lib/stripe_mock/webhook_fixtures/payment_intent.processing.json b/lib/stripe_mock/webhook_fixtures/payment_intent.processing.json new file mode 100644 index 0000000..69c7181 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/payment_intent.processing.json @@ -0,0 +1,162 @@ +{ + "id": "evt_00000000000000", + "object": "event", + "api_version": "2018-02-28", + "created": 1578499109, + "data": { + "object": { + "id": "pi_00000000000000", + "object": "payment_intent", + "allowed_source_types": ["card", "sepa_debit"], + "amount": 900, + "amount_capturable": 0, + "amount_received": 0, + "application": null, + "application_fee_amount": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": { + "object": "list", + "data": [ + { + "id": "ch_00000000000000", + "object": "charge", + "amount": 900, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_00000000000000", + "billing_details": { + "address": { + "city": null, + "country": "DE", + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "captured": true, + "created": 1578499109, + "currency": "eur", + "customer": "cus_00000000000000", + "description": null, + "destination": "acct_00000000000000", + "dispute": null, + "disputed": false, + "failure_code": null, + "failure_message": null, + "fraud_details": {}, + "invoice": null, + "livemode": false, + "metadata": {}, + "on_behalf_of": null, + "order": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 40, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": false, + "payment_intent": "pi_00000000000000", + "payment_method": "pm_00000000000000", + "payment_method_details": { + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": null + }, + "country": "US", + "exp_month": 4, + "exp_year": 2024, + "fingerprint": "00000000000000", + "funding": "credit", + "installments": null, + "last4": "4242", + "network": "visa", + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": null, + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/acct_00000000000000/ch_00000000000000/rcpt_00000000000000", + "refunded": false, + "refunds": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/charges/ch_00000000000000/refunds" + }, + "review": null, + "shipping": null, + "source": null, + "source_transfer": null, + "statement_descriptor": "ACME Corp", + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer": "tr_00000000000000", + "transfer_data": { + "amount": null, + "destination": "acct_00000000000000" + }, + "transfer_group": "group_pi_00000000000000" + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/charges?payment_intent=pi_00000000000000" + }, + "client_secret": "pi_00000000000000", + "confirmation_method": "automatic", + "created": 1578499108, + "currency": "eur", + "customer": "cus_00000000000000", + "description": null, + "invoice": null, + "last_payment_error": null, + "livemode": false, + "metadata": {}, + "next_action": null, + "next_source_action": null, + "on_behalf_of": null, + "payment_method": "pm_00000000000000", + "payment_method_options": { + "card": { + "installments": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": ["card", "sepa_debit"], + "receipt_email": null, + "review": null, + "setup_future_usage": null, + "shipping": null, + "source": null, + "statement_descriptor": "ACME Corp", + "statement_descriptor_suffix": null, + "status": "processing", + "transfer_data": null, + "transfer_group": null + } + }, + "livemode": false, + "pending_webhooks": 2, + "request": { + "id": "req_00000000000000", + "idempotency_key": null + }, + "type": "payment_intent.processing" +} diff --git a/lib/stripe_mock/webhook_fixtures/payment_intent.succeeded.json b/lib/stripe_mock/webhook_fixtures/payment_intent.succeeded.json new file mode 100644 index 0000000..7ea356a --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/payment_intent.succeeded.json @@ -0,0 +1,164 @@ +{ + "id": "evt_00000000000000", + "object": "event", + "api_version": "2018-02-28", + "created": 1578499109, + "data": { + "object": { + "id": "pi_00000000000000", + "object": "payment_intent", + "allowed_source_types": ["card", "sepa_debit"], + "amount": 900, + "amount_capturable": 0, + "amount_received": 900, + "application": null, + "application_fee_amount": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": { + "object": "list", + "data": [ + { + "id": "ch_00000000000000", + "object": "charge", + "amount": 900, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_00000000000000", + "billing_details": { + "address": { + "city": null, + "country": "DE", + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "captured": true, + "created": 1578499109, + "currency": "eur", + "customer": "cus_00000000000000", + "description": null, + "destination": "acct_00000000000000", + "dispute": null, + "disputed": false, + "failure_code": null, + "failure_message": null, + "fraud_details": {}, + "invoice": null, + "livemode": false, + "metadata": {}, + "on_behalf_of": null, + "order": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 40, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": true, + "payment_intent": "pi_00000000000000", + "payment_method": "pm_00000000000000", + "payment_method_details": { + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": null + }, + "country": "US", + "exp_month": 4, + "exp_year": 2024, + "fingerprint": "00000000000000", + "funding": "credit", + "installments": null, + "last4": "4242", + "network": "visa", + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": null, + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/acct_00000000000000/ch_00000000000000/rcpt_00000000000000", + "refunded": false, + "refunds": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/charges/ch_00000000000000/refunds" + }, + "review": null, + "shipping": null, + "source": null, + "source_transfer": null, + "statement_descriptor": "ACME Corp", + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer": "tr_00000000000000", + "transfer_data": { + "amount": null, + "destination": "acct_00000000000000" + }, + "transfer_group": "group_pi_00000000000000" + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/charges?payment_intent=pi_00000000000000" + }, + "client_secret": "pi_00000000000000", + "confirmation_method": "automatic", + "created": 1578499108, + "currency": "eur", + "customer": "cus_00000000000000", + "description": null, + "invoice": null, + "last_payment_error": null, + "livemode": false, + "metadata": {}, + "next_action": null, + "next_source_action": null, + "on_behalf_of": null, + "payment_method": "pm_00000000000000", + "payment_method_options": { + "card": { + "installments": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": ["card", "sepa_debit"], + "receipt_email": null, + "review": null, + "setup_future_usage": null, + "shipping": null, + "source": null, + "statement_descriptor": "ACME Corp", + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": { + "destination": "acct_00000000000000" + }, + "transfer_group": "group_pi_00000000000000" + } + }, + "livemode": false, + "pending_webhooks": 2, + "request": { + "id": "req_00000000000000", + "idempotency_key": null + }, + "type": "payment_intent.succeeded" +} diff --git a/lib/stripe_mock/webhook_fixtures/plan.created.json b/lib/stripe_mock/webhook_fixtures/plan.created.json new file mode 100644 index 0000000..64e94a5 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/plan.created.json @@ -0,0 +1,20 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "plan.created", + "object": "event", + "data": { + "object": { + "interval": "month", + "product": "pr_00000000000000", + "amount": 100, + "currency": "usd", + "id": "fkx0AFo_00000000000000", + "object": "plan", + "livemode": false, + "interval_count": 1, + "trial_period_days": null + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/plan.deleted.json b/lib/stripe_mock/webhook_fixtures/plan.deleted.json new file mode 100644 index 0000000..3669585 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/plan.deleted.json @@ -0,0 +1,20 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "plan.deleted", + "object": "event", + "data": { + "object": { + "interval": "month", + "product": "pr_00000000000000", + "amount": 100, + "currency": "usd", + "id": "fkx0AFo_00000000000000", + "object": "plan", + "livemode": false, + "interval_count": 1, + "trial_period_days": null + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/plan.updated.json b/lib/stripe_mock/webhook_fixtures/plan.updated.json new file mode 100644 index 0000000..9f241e8 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/plan.updated.json @@ -0,0 +1,23 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "plan.updated", + "object": "event", + "data": { + "object": { + "interval": "month", + "product": "pr_00000000000000", + "amount": 100, + "currency": "usd", + "id": "fkx0AFo_00000000000000", + "object": "plan", + "livemode": false, + "interval_count": 1, + "trial_period_days": null + }, + "previous_attributes": { + "name": "Old name" + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/product.created.json b/lib/stripe_mock/webhook_fixtures/product.created.json new file mode 100644 index 0000000..5dc5ec9 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/product.created.json @@ -0,0 +1,34 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "product.created", + "object": "event", + "data": { + "object": { + "id": "prod_00000000000000", + "object": "product", + "active": true, + "attributes": [ + ], + "caption": null, + "created": 1558795883, + "deactivate_on": [ + ], + "description": null, + "images": [ + ], + "livemode": false, + "metadata": { + }, + "name": "Test Product", + "package_dimensions": null, + "shippable": null, + "statement_descriptor": null, + "type": "service", + "unit_label": null, + "updated": 1558795883, + "url": null + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/product.deleted.json b/lib/stripe_mock/webhook_fixtures/product.deleted.json new file mode 100644 index 0000000..a0499c5 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/product.deleted.json @@ -0,0 +1,34 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "product.deleted", + "object": "event", + "data": { + "object": { + "id": "prod_00000000000000", + "object": "product", + "active": true, + "attributes": [ + ], + "caption": null, + "created": 1558795883, + "deactivate_on": [ + ], + "description": null, + "images": [ + ], + "livemode": false, + "metadata": { + }, + "name": "Test Product", + "package_dimensions": null, + "shippable": null, + "statement_descriptor": null, + "type": "service", + "unit_label": null, + "updated": 1558795883, + "url": null + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/product.updated.json b/lib/stripe_mock/webhook_fixtures/product.updated.json new file mode 100644 index 0000000..37c8024 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/product.updated.json @@ -0,0 +1,38 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "product.updated", + "object": "event", + "data": { + "object": { + "id": "prod_00000000000000", + "object": "product", + "active": true, + "attributes": [ + ], + "caption": null, + "created": 1558795883, + "deactivate_on": [ + ], + "description": null, + "images": [ + ], + "livemode": false, + "metadata": { + }, + "name": "Test Product", + "package_dimensions": null, + "shippable": null, + "statement_descriptor": null, + "type": "service", + "unit_label": null, + "updated": 1558795883, + "url": null + }, + "previous_attributes": { + "name": "Product Test", + "updated": 1558873981 + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/transfer.created.json b/lib/stripe_mock/webhook_fixtures/transfer.created.json new file mode 100644 index 0000000..4431164 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/transfer.created.json @@ -0,0 +1,89 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "transfer.created", + "object": "event", + "data": { + "object": { + "id": "tr_00000000000000", + "object": "transfer", + "date": 1381104000, + "livemode": false, + "amount": 67, + "currency": "usd", + "status": "pending", + "balance_transaction": "txn_00000000000000", + "summary": { + "charge_gross": 100, + "charge_fees": 33, + "charge_fee_details": [ + { + "amount": 33, + "currency": "usd", + "type": "stripe_fee", + "description": null, + "application": null + } + ], + "refund_gross": 0, + "refund_fees": 0, + "refund_fee_details": [ + + ], + "adjustment_gross": 0, + "adjustment_fees": 0, + "adjustment_fee_details": [ + + ], + "validation_fees": 0, + "validation_count": 0, + "charge_count": 1, + "refund_count": 0, + "adjustment_count": 0, + "net": 67, + "currency": "usd", + "collected_fee_gross": 0, + "collected_fee_count": 0, + "collected_fee_refund_gross": 0, + "collected_fee_refund_count": 0 + }, + "transactions": { + "object": "list", + "count": 1, + "url": "/v1/transfers/tr_2h8RC13PPvwDZs/transactions", + "has_more": false, + "data": [ + { + "id": "ch_2fb4RERw49oI8s", + "type": "charge", + "amount": 100, + "currency": "usd", + "net": 67, + "created": 1380582860, + "description": null, + "fee": 33, + "fee_details": [ + { + "amount": 33, + "currency": "usd", + "type": "stripe_fee", + "description": "Stripe processing fees", + "application": null + } + ] + } + ] + }, + "other_transfers": [ + "tr_2h8RC13PPvwDZs" + ], + "account": null, + "description": "STRIPE TRANSFER", + "metadata": { + }, + "statement_descriptor": null, + "recipient": null + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/transfer.failed.json b/lib/stripe_mock/webhook_fixtures/transfer.failed.json new file mode 100644 index 0000000..32f5398 --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/transfer.failed.json @@ -0,0 +1,89 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "transfer.failed", + "object": "event", + "data": { + "object": { + "id": "tr_00000000000000", + "object": "transfer", + "date": 1381104000, + "livemode": false, + "amount": 67, + "currency": "usd", + "status": "failed", + "balance_transaction": "txn_00000000000000", + "summary": { + "charge_gross": 100, + "charge_fees": 33, + "charge_fee_details": [ + { + "amount": 33, + "currency": "usd", + "type": "stripe_fee", + "description": null, + "application": null + } + ], + "refund_gross": 0, + "refund_fees": 0, + "refund_fee_details": [ + + ], + "adjustment_gross": 0, + "adjustment_fees": 0, + "adjustment_fee_details": [ + + ], + "validation_fees": 0, + "validation_count": 0, + "charge_count": 1, + "refund_count": 0, + "adjustment_count": 0, + "net": 67, + "currency": "usd", + "collected_fee_gross": 0, + "collected_fee_count": 0, + "collected_fee_refund_gross": 0, + "collected_fee_refund_count": 0 + }, + "transactions": { + "object": "list", + "count": 1, + "url": "/v1/transfers/tr_2h8RC13PPvwDZs/transactions", + "has_more": false, + "data": [ + { + "id": "ch_2fb4RERw49oI8s", + "type": "charge", + "amount": 100, + "currency": "usd", + "net": 67, + "created": 1380582860, + "description": null, + "fee": 33, + "fee_details": [ + { + "amount": 33, + "currency": "usd", + "type": "stripe_fee", + "description": "Stripe processing fees", + "application": null + } + ] + } + ] + }, + "other_transfers": [ + "tr_2h8RC13PPvwDZs" + ], + "account": null, + "description": "STRIPE TRANSFER", + "metadata": { + }, + "statement_descriptor": null, + "recipient": null + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/transfer.paid.json b/lib/stripe_mock/webhook_fixtures/transfer.paid.json new file mode 100644 index 0000000..537213c --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/transfer.paid.json @@ -0,0 +1,89 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "transfer.paid", + "object": "event", + "data": { + "object": { + "id": "tr_00000000000000", + "object": "transfer", + "date": 1381104000, + "livemode": false, + "amount": 67, + "currency": "usd", + "status": "paid", + "balance_transaction": "txn_00000000000000", + "summary": { + "charge_gross": 100, + "charge_fees": 33, + "charge_fee_details": [ + { + "amount": 33, + "currency": "usd", + "type": "stripe_fee", + "description": null, + "application": null + } + ], + "refund_gross": 0, + "refund_fees": 0, + "refund_fee_details": [ + + ], + "adjustment_gross": 0, + "adjustment_fees": 0, + "adjustment_fee_details": [ + + ], + "validation_fees": 0, + "validation_count": 0, + "charge_count": 1, + "refund_count": 0, + "adjustment_count": 0, + "net": 67, + "currency": "usd", + "collected_fee_gross": 0, + "collected_fee_count": 0, + "collected_fee_refund_gross": 0, + "collected_fee_refund_count": 0 + }, + "transactions": { + "object": "list", + "count": 1, + "url": "/v1/transfers/tr_2h8RC13PPvwDZs/transactions", + "has_more": false, + "data": [ + { + "id": "ch_2fb4RERw49oI8s", + "type": "charge", + "amount": 100, + "currency": "usd", + "net": 67, + "created": 1380582860, + "description": null, + "fee": 33, + "fee_details": [ + { + "amount": 33, + "currency": "usd", + "type": "stripe_fee", + "description": "Stripe processing fees", + "application": null + } + ] + } + ] + }, + "other_transfers": [ + "tr_2h8RC13PPvwDZs" + ], + "account": null, + "description": "STRIPE TRANSFER", + "metadata": { + }, + "statement_descriptor": null, + "recipient": null + } + } +} diff --git a/lib/stripe_mock/webhook_fixtures/transfer.updated.json b/lib/stripe_mock/webhook_fixtures/transfer.updated.json new file mode 100644 index 0000000..7e825fd --- /dev/null +++ b/lib/stripe_mock/webhook_fixtures/transfer.updated.json @@ -0,0 +1,92 @@ +{ + "created": 1326853478, + "livemode": false, + "id": "evt_00000000000000", + "type": "transfer.updated", + "object": "event", + "data": { + "object": { + "id": "tr_00000000000000", + "object": "transfer", + "date": 1381104000, + "livemode": false, + "amount": 67, + "currency": "usd", + "status": "pending", + "balance_transaction": "txn_00000000000000", + "summary": { + "charge_gross": 100, + "charge_fees": 33, + "charge_fee_details": [ + { + "amount": 33, + "currency": "usd", + "type": "stripe_fee", + "description": null, + "application": null + } + ], + "refund_gross": 0, + "refund_fees": 0, + "refund_fee_details": [ + + ], + "adjustment_gross": 0, + "adjustment_fees": 0, + "adjustment_fee_details": [ + + ], + "validation_fees": 0, + "validation_count": 0, + "charge_count": 1, + "refund_count": 0, + "adjustment_count": 0, + "net": 67, + "currency": "usd", + "collected_fee_gross": 0, + "collected_fee_count": 0, + "collected_fee_refund_gross": 0, + "collected_fee_refund_count": 0 + }, + "transactions": { + "object": "list", + "count": 1, + "url": "/v1/transfers/tr_2h8RC13PPvwDZs/transactions", + "has_more": false, + "data": [ + { + "id": "ch_2fb4RERw49oI8s", + "type": "charge", + "amount": 100, + "currency": "usd", + "net": 67, + "created": 1380582860, + "description": null, + "fee": 33, + "fee_details": [ + { + "amount": 33, + "currency": "usd", + "type": "stripe_fee", + "description": "Stripe processing fees", + "application": null + } + ] + } + ] + }, + "other_transfers": [ + "tr_2h8RC13PPvwDZs" + ], + "account": null, + "description": "STRIPE TRANSFER", + "metadata": { + }, + "statement_descriptor": null, + "recipient": null + }, + "previous_attributes": { + "amount": 123 + } + } +} diff --git a/lib/trollop.rb b/lib/trollop.rb new file mode 100644 index 0000000..d7f7bcc --- /dev/null +++ b/lib/trollop.rb @@ -0,0 +1,782 @@ +## lib/trollop.rb -- trollop command-line processing library +## Author:: William Morgan (mailto: wmorgan-trollop@masanjin.net) +## Copyright:: Copyright 2007 William Morgan +## License:: the same terms as ruby itself + +require 'date' + +module Trollop + +VERSION = "2.0" + +## Thrown by Parser in the event of a commandline error. Not needed if +## you're using the Trollop::options entry. +class CommandlineError < StandardError; end + +## Thrown by Parser if the user passes in '-h' or '--help'. Handled +## automatically by Trollop#options. +class HelpNeeded < StandardError; end + +## Thrown by Parser if the user passes in '-h' or '--version'. Handled +## automatically by Trollop#options. +class VersionNeeded < StandardError; end + +## Regex for floating point numbers +FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))([eE][-+]?[\d]+)?$/ + +## Regex for parameters +PARAM_RE = /^-(-|\.$|[^\d\.])/ + +## The commandline parser. In typical usage, the methods in this class +## will be handled internally by Trollop::options. In this case, only the +## #opt, #banner and #version, #depends, and #conflicts methods will +## typically be called. +## +## If you want to instantiate this class yourself (for more complicated +## argument-parsing logic), call #parse to actually produce the output hash, +## and consider calling it from within +## Trollop::with_standard_exception_handling. +class Parser + + ## The set of values that indicate a flag option when passed as the + ## +:type+ parameter of #opt. + FLAG_TYPES = [:flag, :bool, :boolean] + + ## The set of values that indicate a single-parameter (normal) option when + ## passed as the +:type+ parameter of #opt. + ## + ## A value of +io+ corresponds to a readable IO resource, including + ## a filename, URI, or the strings 'stdin' or '-'. + SINGLE_ARG_TYPES = [:int, :integer, :string, :double, :float, :io, :date] + + ## The set of values that indicate a multiple-parameter option (i.e., that + ## takes multiple space-separated values on the commandline) when passed as + ## the +:type+ parameter of #opt. + MULTI_ARG_TYPES = [:ints, :integers, :strings, :doubles, :floats, :ios, :dates] + + ## The complete set of legal values for the +:type+ parameter of #opt. + TYPES = FLAG_TYPES + SINGLE_ARG_TYPES + MULTI_ARG_TYPES + + INVALID_SHORT_ARG_REGEX = /[\d-]/ #:nodoc: + + ## The values from the commandline that were not interpreted by #parse. + attr_reader :leftovers + + ## The complete configuration hashes for each option. (Mainly useful + ## for testing.) + attr_reader :specs + + ## Initializes the parser, and instance-evaluates any block given. + def initialize *a, &b + @version = nil + @leftovers = [] + @specs = {} + @long = {} + @short = {} + @order = [] + @constraints = [] + @stop_words = [] + @stop_on_unknown = false + + #instance_eval(&b) if b # can't take arguments + cloaker(&b).bind(self).call(*a) if b + end + + ## Define an option. +name+ is the option name, a unique identifier + ## for the option that you will use internally, which should be a + ## symbol or a string. +desc+ is a string description which will be + ## displayed in help messages. + ## + ## Takes the following optional arguments: + ## + ## [+:long+] Specify the long form of the argument, i.e. the form with two dashes. If unspecified, will be automatically derived based on the argument name by turning the +name+ option into a string, and replacing any _'s by -'s. + ## [+:short+] Specify the short form of the argument, i.e. the form with one dash. If unspecified, will be automatically derived from +name+. + ## [+:type+] Require that the argument take a parameter or parameters of type +type+. For a single parameter, the value can be a member of +SINGLE_ARG_TYPES+, or a corresponding Ruby class (e.g. +Integer+ for +:int+). For multiple-argument parameters, the value can be any member of +MULTI_ARG_TYPES+ constant. If unset, the default argument type is +:flag+, meaning that the argument does not take a parameter. The specification of +:type+ is not necessary if a +:default+ is given. + ## [+:default+] Set the default value for an argument. Without a default value, the hash returned by #parse (and thus Trollop::options) will have a +nil+ value for this key unless the argument is given on the commandline. The argument type is derived automatically from the class of the default value given, so specifying a +:type+ is not necessary if a +:default+ is given. (But see below for an important caveat when +:multi+: is specified too.) If the argument is a flag, and the default is set to +true+, then if it is specified on the the commandline the value will be +false+. + ## [+:required+] If set to +true+, the argument must be provided on the commandline. + ## [+:multi+] If set to +true+, allows multiple occurrences of the option on the commandline. Otherwise, only a single instance of the option is allowed. (Note that this is different from taking multiple parameters. See below.) + ## + ## Note that there are two types of argument multiplicity: an argument + ## can take multiple values, e.g. "--arg 1 2 3". An argument can also + ## be allowed to occur multiple times, e.g. "--arg 1 --arg 2". + ## + ## Arguments that take multiple values should have a +:type+ parameter + ## drawn from +MULTI_ARG_TYPES+ (e.g. +:strings+), or a +:default:+ + ## value of an array of the correct type (e.g. [String]). The + ## value of this argument will be an array of the parameters on the + ## commandline. + ## + ## Arguments that can occur multiple times should be marked with + ## +:multi+ => +true+. The value of this argument will also be an array. + ## In contrast with regular non-multi options, if not specified on + ## the commandline, the default value will be [], not nil. + ## + ## These two attributes can be combined (e.g. +:type+ => +:strings+, + ## +:multi+ => +true+), in which case the value of the argument will be + ## an array of arrays. + ## + ## There's one ambiguous case to be aware of: when +:multi+: is true and a + ## +:default+ is set to an array (of something), it's ambiguous whether this + ## is a multi-value argument as well as a multi-occurrence argument. + ## In thise case, Trollop assumes that it's not a multi-value argument. + ## If you want a multi-value, multi-occurrence argument with a default + ## value, you must specify +:type+ as well. + + def opt name, desc="", opts={} + raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? name + + ## fill in :type + opts[:type] = # normalize + case opts[:type] + when :boolean, :bool; :flag + when :integer; :int + when :integers; :ints + when :double; :float + when :doubles; :floats + when Class + case opts[:type].name + when 'TrueClass', 'FalseClass'; :flag + when 'String'; :string + when 'Integer'; :int + when 'Float'; :float + when 'IO'; :io + when 'Date'; :date + else + raise ArgumentError, "unsupported argument type '#{opts[:type].class.name}'" + end + when nil; nil + else + raise ArgumentError, "unsupported argument type '#{opts[:type]}'" unless TYPES.include?(opts[:type]) + opts[:type] + end + + ## for options with :multi => true, an array default doesn't imply + ## a multi-valued argument. for that you have to specify a :type + ## as well. (this is how we disambiguate an ambiguous situation; + ## see the docs for Parser#opt for details.) + disambiguated_default = if opts[:multi] && opts[:default].is_a?(Array) && !opts[:type] + opts[:default].first + else + opts[:default] + end + + type_from_default = + case disambiguated_default + when Integer; :int + when Numeric; :float + when TrueClass, FalseClass; :flag + when String; :string + when IO; :io + when Date; :date + when Array + if opts[:default].empty? + raise ArgumentError, "multiple argument type cannot be deduced from an empty array for '#{opts[:default][0].class.name}'" + end + case opts[:default][0] # the first element determines the types + when Integer; :ints + when Numeric; :floats + when String; :strings + when IO; :ios + when Date; :dates + else + raise ArgumentError, "unsupported multiple argument type '#{opts[:default][0].class.name}'" + end + when nil; nil + else + raise ArgumentError, "unsupported argument type '#{opts[:default].class.name}'" + end + + raise ArgumentError, ":type specification and default type don't match (default type is #{type_from_default})" if opts[:type] && type_from_default && opts[:type] != type_from_default + + opts[:type] = opts[:type] || type_from_default || :flag + + ## fill in :long + opts[:long] = opts[:long] ? opts[:long].to_s : name.to_s.gsub("_", "-") + opts[:long] = case opts[:long] + when /^--([^-].*)$/; $1 + when /^[^-]/; opts[:long] + else; raise ArgumentError, "invalid long option name #{opts[:long].inspect}" + end + raise ArgumentError, "long option name #{opts[:long].inspect} is already taken; please specify a (different) :long" if @long[opts[:long]] + + ## fill in :short + opts[:short] = opts[:short].to_s if opts[:short] unless opts[:short] == :none + opts[:short] = case opts[:short] + when /^-(.)$/; $1 + when nil, :none, /^.$/; opts[:short] + else raise ArgumentError, "invalid short option name '#{opts[:short].inspect}'" + end + + if opts[:short] + raise ArgumentError, "short option name #{opts[:short].inspect} is already taken; please specify a (different) :short" if @short[opts[:short]] + raise ArgumentError, "a short option name can't be a number or a dash" if opts[:short] =~ INVALID_SHORT_ARG_REGEX + end + + ## fill in :default for flags + opts[:default] = false if opts[:type] == :flag && opts[:default].nil? + + ## autobox :default for :multi (multi-occurrence) arguments + opts[:default] = [opts[:default]] if opts[:default] && opts[:multi] && !opts[:default].is_a?(Array) + + ## fill in :multi + opts[:multi] ||= false + + opts[:desc] ||= desc + @long[opts[:long]] = name + @short[opts[:short]] = name if opts[:short] && opts[:short] != :none + @specs[name] = opts + @order << [:opt, name] + end + + ## Sets the version string. If set, the user can request the version + ## on the commandline. Should probably be of the form " + ## ". + def version s=nil; @version = s if s; @version end + + ## Adds text to the help display. Can be interspersed with calls to + ## #opt to build a multi-section help page. + def banner s; @order << [:text, s] end + alias :text :banner + + ## Marks two (or more!) options as requiring each other. Only handles + ## undirected (i.e., mutual) dependencies. Directed dependencies are + ## better modeled with Trollop::die. + def depends *syms + syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] } + @constraints << [:depends, syms] + end + + ## Marks two (or more!) options as conflicting. + def conflicts *syms + syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] } + @constraints << [:conflicts, syms] + end + + ## Defines a set of words which cause parsing to terminate when + ## encountered, such that any options to the left of the word are + ## parsed as usual, and options to the right of the word are left + ## intact. + ## + ## A typical use case would be for subcommand support, where these + ## would be set to the list of subcommands. A subsequent Trollop + ## invocation would then be used to parse subcommand options, after + ## shifting the subcommand off of ARGV. + def stop_on *words + @stop_words = [*words].flatten + end + + ## Similar to #stop_on, but stops on any unknown word when encountered + ## (unless it is a parameter for an argument). This is useful for + ## cases where you don't know the set of subcommands ahead of time, + ## i.e., without first parsing the global options. + def stop_on_unknown + @stop_on_unknown = true + end + + ## Parses the commandline. Typically called by Trollop::options, + ## but you can call it directly if you need more control. + ## + ## throws CommandlineError, HelpNeeded, and VersionNeeded exceptions. + def parse cmdline=ARGV + vals = {} + required = {} + + opt :version, "Print version and exit" if @version unless @specs[:version] || @long["version"] + opt :help, "Show this message" unless @specs[:help] || @long["help"] + + @specs.each do |sym, opts| + required[sym] = true if opts[:required] + vals[sym] = opts[:default] + vals[sym] = [] if opts[:multi] && !opts[:default] # multi arguments default to [], not nil + end + + resolve_default_short_options! + + ## resolve symbols + given_args = {} + @leftovers = each_arg cmdline do |arg, params| + ## handle --no- forms + arg, negative_given = if arg =~ /^--no-([^-]\S*)$/ + ["--#{$1}", true] + else + [arg, false] + end + + sym = case arg + when /^-([^-])$/; @short[$1] + when /^--([^-]\S*)$/; @long[$1] || @long["no-#{$1}"] + else; raise CommandlineError, "invalid argument syntax: '#{arg}'" + end + + sym = nil if arg =~ /--no-/ # explicitly invalidate --no-no- arguments + + raise CommandlineError, "unknown argument '#{arg}'" unless sym + + if given_args.include?(sym) && !@specs[sym][:multi] + raise CommandlineError, "option '#{arg}' specified multiple times" + end + + given_args[sym] ||= {} + given_args[sym][:arg] = arg + given_args[sym][:negative_given] = negative_given + given_args[sym][:params] ||= [] + + # The block returns the number of parameters taken. + num_params_taken = 0 + + unless params.nil? + if SINGLE_ARG_TYPES.include?(@specs[sym][:type]) + given_args[sym][:params] << params[0, 1] # take the first parameter + num_params_taken = 1 + elsif MULTI_ARG_TYPES.include?(@specs[sym][:type]) + given_args[sym][:params] << params # take all the parameters + num_params_taken = params.size + end + end + + num_params_taken + end + + ## check for version and help args + raise VersionNeeded if given_args.include? :version + raise HelpNeeded if given_args.include? :help + + ## check constraint satisfaction + @constraints.each do |type, syms| + constraint_sym = syms.find { |sym| given_args[sym] } + next unless constraint_sym + + case type + when :depends + syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} requires --#{@specs[sym][:long]}" unless given_args.include? sym } + when :conflicts + syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} conflicts with --#{@specs[sym][:long]}" if given_args.include?(sym) && (sym != constraint_sym) } + end + end + + required.each do |sym, val| + raise CommandlineError, "option --#{@specs[sym][:long]} must be specified" unless given_args.include? sym + end + + ## parse parameters + given_args.each do |sym, given_data| + arg, params, negative_given = given_data.values_at :arg, :params, :negative_given + + opts = @specs[sym] + raise CommandlineError, "option '#{arg}' needs a parameter" if params.empty? && opts[:type] != :flag + + vals["#{sym}_given".intern] = true # mark argument as specified on the commandline + + case opts[:type] + when :flag + vals[sym] = (sym.to_s =~ /^no_/ ? negative_given : !negative_given) + when :int, :ints + vals[sym] = params.map { |pg| pg.map { |p| parse_integer_parameter p, arg } } + when :float, :floats + vals[sym] = params.map { |pg| pg.map { |p| parse_float_parameter p, arg } } + when :string, :strings + vals[sym] = params.map { |pg| pg.map { |p| p.to_s } } + when :io, :ios + vals[sym] = params.map { |pg| pg.map { |p| parse_io_parameter p, arg } } + when :date, :dates + vals[sym] = params.map { |pg| pg.map { |p| parse_date_parameter p, arg } } + end + + if SINGLE_ARG_TYPES.include?(opts[:type]) + unless opts[:multi] # single parameter + vals[sym] = vals[sym][0][0] + else # multiple options, each with a single parameter + vals[sym] = vals[sym].map { |p| p[0] } + end + elsif MULTI_ARG_TYPES.include?(opts[:type]) && !opts[:multi] + vals[sym] = vals[sym][0] # single option, with multiple parameters + end + # else: multiple options, with multiple parameters + end + + ## modify input in place with only those + ## arguments we didn't process + cmdline.clear + @leftovers.each { |l| cmdline << l } + + ## allow openstruct-style accessors + class << vals + def method_missing(m, *args) + self[m] || self[m.to_s] + end + end + vals + end + + def parse_date_parameter param, arg #:nodoc: + begin + begin + time = Chronic.parse(param) + rescue NameError + # chronic is not available + end + time ? Date.new(time.year, time.month, time.day) : Date.parse(param) + rescue ArgumentError + raise CommandlineError, "option '#{arg}' needs a date" + end + end + + ## Print the help message to +stream+. + def educate stream=$stdout + width # hack: calculate it now; otherwise we have to be careful not to + # call this unless the cursor's at the beginning of a line. + left = {} + @specs.each do |name, spec| + left[name] = "--#{spec[:long]}" + + (spec[:type] == :flag && spec[:default] ? ", --no-#{spec[:long]}" : "") + + (spec[:short] && spec[:short] != :none ? ", -#{spec[:short]}" : "") + + case spec[:type] + when :flag; "" + when :int; " " + when :ints; " " + when :string; " " + when :strings; " " + when :float; " " + when :floats; " " + when :io; " " + when :ios; " " + when :date; " " + when :dates; " " + end + end + + leftcol_width = left.values.map { |s| s.length }.max || 0 + rightcol_start = leftcol_width + 6 # spaces + + unless @order.size > 0 && @order.first.first == :text + stream.puts "#@version\n" if @version + stream.puts "Options:" + end + + @order.each do |what, opt| + if what == :text + stream.puts wrap(opt) + next + end + + spec = @specs[opt] + stream.printf " %#{leftcol_width}s: ", left[opt] + desc = spec[:desc] + begin + default_s = case spec[:default] + when $stdout; "" + when $stdin; "" + when $stderr; "" + when Array + spec[:default].join(", ") + else + spec[:default].to_s + end + + if spec[:default] + if spec[:desc] =~ /\.$/ + " (Default: #{default_s})" + else + " (default: #{default_s})" + end + else + "" + end + end + stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start) + end + end + + def width #:nodoc: + @width ||= if $stdout.tty? + begin + require 'curses' + Curses::init_screen + x = Curses::cols + Curses::close_screen + x + rescue Exception + 80 + end + else + 80 + end + end + + def wrap str, opts={} # :nodoc: + if str == "" + [""] + else + str.split("\n").map { |s| wrap_line s, opts }.flatten + end + end + + ## The per-parser version of Trollop::die (see that for documentation). + def die arg, msg + if msg + $stderr.puts "Error: argument --#{@specs[arg][:long]} #{msg}." + else + $stderr.puts "Error: #{arg}." + end + $stderr.puts "Try --help for help." + exit(-1) + end + +private + + ## yield successive arg, parameter pairs + def each_arg args + remains = [] + i = 0 + + until i >= args.length + if @stop_words.member? args[i] + remains += args[i .. -1] + return remains + end + case args[i] + when /^--$/ # arg terminator + remains += args[(i + 1) .. -1] + return remains + when /^--(\S+?)=(.*)$/ # long argument with equals + yield "--#{$1}", [$2] + i += 1 + when /^--(\S+)$/ # long argument + params = collect_argument_parameters(args, i + 1) + unless params.empty? + num_params_taken = yield args[i], params + unless num_params_taken + if @stop_on_unknown + remains += args[i + 1 .. -1] + return remains + else + remains += params + end + end + i += 1 + num_params_taken + else # long argument no parameter + yield args[i], nil + i += 1 + end + when /^-(\S+)$/ # one or more short arguments + shortargs = $1.split(//) + shortargs.each_with_index do |a, j| + if j == (shortargs.length - 1) + params = collect_argument_parameters(args, i + 1) + unless params.empty? + num_params_taken = yield "-#{a}", params + unless num_params_taken + if @stop_on_unknown + remains += args[i + 1 .. -1] + return remains + else + remains += params + end + end + i += 1 + num_params_taken + else # argument no parameter + yield "-#{a}", nil + i += 1 + end + else + yield "-#{a}", nil + end + end + else + if @stop_on_unknown + remains += args[i .. -1] + return remains + else + remains << args[i] + i += 1 + end + end + end + + remains + end + + def parse_integer_parameter param, arg + raise CommandlineError, "option '#{arg}' needs an integer" unless param =~ /^\d+$/ + param.to_i + end + + def parse_float_parameter param, arg + raise CommandlineError, "option '#{arg}' needs a floating-point number" unless param =~ FLOAT_RE + param.to_f + end + + def parse_io_parameter param, arg + case param + when /^(stdin|-)$/i; $stdin + else + require 'open-uri' + begin + open param + rescue SystemCallError => e + raise CommandlineError, "file or url for option '#{arg}' cannot be opened: #{e.message}" + end + end + end + + def collect_argument_parameters args, start_at + params = [] + pos = start_at + while args[pos] && args[pos] !~ PARAM_RE && !@stop_words.member?(args[pos]) do + params << args[pos] + pos += 1 + end + params + end + + def resolve_default_short_options! + @order.each do |type, name| + next unless type == :opt + opts = @specs[name] + next if opts[:short] + + c = opts[:long].split(//).find { |d| d !~ INVALID_SHORT_ARG_REGEX && !@short.member?(d) } + if c # found a character to use + opts[:short] = c + @short[c] = name + end + end + end + + def wrap_line str, opts={} + prefix = opts[:prefix] || 0 + width = opts[:width] || (self.width - 1) + start = 0 + ret = [] + until start > str.length + nextt = + if start + width >= str.length + str.length + else + x = str.rindex(/\s/, start + width) + x = str.index(/\s/, start) if x && x < start + x || str.length + end + ret << (ret.empty? ? "" : " " * prefix) + str[start ... nextt] + start = nextt + 1 + end + ret + end + + ## instance_eval but with ability to handle block arguments + ## thanks to _why: http://redhanded.hobix.com/inspect/aBlockCostume.html + def cloaker &b + (class << self; self; end).class_eval do + define_method :cloaker_, &b + meth = instance_method :cloaker_ + remove_method :cloaker_ + meth + end + end +end + +## The easy, syntactic-sugary entry method into Trollop. Creates a Parser, +## passes the block to it, then parses +args+ with it, handling any errors or +## requests for help or version information appropriately (and then exiting). +## Modifies +args+ in place. Returns a hash of option values. +## +## The block passed in should contain zero or more calls to +opt+ +## (Parser#opt), zero or more calls to +text+ (Parser#text), and +## probably a call to +version+ (Parser#version). +## +## The returned block contains a value for every option specified with +## +opt+. The value will be the value given on the commandline, or the +## default value if the option was not specified on the commandline. For +## every option specified on the commandline, a key "