{"id":20181860,"url":"https://github.com/nichoth/whammy","last_synced_at":"2026-05-11T10:35:39.550Z","repository":{"id":38260157,"uuid":"270456472","full_name":"nichoth/whammy","owner":"nichoth","description":null,"archived":false,"fork":false,"pushed_at":"2024-01-09T00:39:46.000Z","size":13130,"stargazers_count":1,"open_issues_count":28,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-13T17:24:21.349Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://github.com/nichoth/whammy#readme","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nichoth.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-06-07T23:01:46.000Z","updated_at":"2023-11-17T13:01:58.000Z","dependencies_parsed_at":"2024-11-14T07:03:42.058Z","dependency_job_id":null,"html_url":"https://github.com/nichoth/whammy","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nichoth%2Fwhammy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nichoth%2Fwhammy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nichoth%2Fwhammy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nichoth%2Fwhammy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nichoth","download_url":"https://codeload.github.com/nichoth/whammy/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241616689,"owners_count":19991543,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-14T02:36:55.338Z","updated_at":"2026-05-11T10:35:39.484Z","avatar_url":"https://github.com/nichoth.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# whammy\nTrying things with netlify \u0026 stripe\n\n\nStart the netlify local dev server, watch css and js files\n```\nnpm start\n```\n\nBuild everything, the same way it will be deployed\n```\nnpm run build\n```\n\nTests for the buy things page\n```\nnpm test\n```\n\nOpen the cypress test gui\n```\nnpm run test:open\n```\n\n\n\n-------------------------\n\n## test cards\n```\n4242424242424242\n```\nSucceeds and immediately processes the payment.\n\n```\n4000000000003220\n```\n3D Secure 2 authentication must be completed for a successful payment.\n\n```\n4000000000009995\n```\nAlways fails with a decline code of insufficient_funds.\n\n```\n4000002760003184\n```\nA test card number that requires authentication.\n\n--------------------------\n\n[Products and Prices](https://stripe.com/docs/billing/prices-guide)\n\u003e If you only have a few products, you should create and manage them in the Dashboard \n\n[List all products](https://stripe.com/docs/api/products/list)\n```js\nrequire('dotenv').config()\nvar stripe = require('stripe')(process.env.STRIPE_SECRET);\n\nstripe.products.list({ limit: 3 }, function (err, products) {\n    // asynchronously called\n});\n```\n\n[Create a customer](https://stripe.com/docs/billing/prices-guide#create-customer)\n\u003e Before billing a customer, you need to create a Customer object that you can configure with a name, email, and payment method.\n```js\n// Set your secret key. Remember to switch to your live secret key in production!\n// See your keys here: https://dashboard.stripe.com/account/apikeys\nconst stripe = require('stripe')('sk_test_51GrU9fGmvbUUvDLHxIdVkGP7SsJv9Re4AY6gJ4E9rR55pEIozVyX0BF2H8CO2mpYuZg3eDr4ftjjmTD9GNKsJoMk00wn6cXykX');\n\nconst customer = await stripe.customers.create({\n  email: 'jenny.rosen@example.com',\n  payment_method: 'pm_card_visa',\n  invoice_settings: {\n    default_payment_method: 'pm_card_visa',\n  },\n});\n```\n\n------------------------------------\n\n[checkout](https://stripe.com/docs/payments/checkout)\n\n-----------------------------------------\n\n## TODO today\nfor the checkout\n- [x] [Retrieve a product](https://stripe.com/docs/api/products/retrieve)\n\n```js\nvar stripe = require('stripe')('sk_test_51GrU9fGmvbUUvDLHxIdVkGP7SsJv9Re4AY6gJ4E9rR55pEIozVyX0BF2H8CO2mpYuZg3eDr4ftjjmTD9GNKsJoMk00wn6cXykX');\n\nstripe.products.retrieve('prod_HQTNO4cLeDwzDX', function (err, product) {\n    // asynchronously called\n});\n```\n\n-----------------------\n\n## todo\n- [x] redirect to a good place after purchase\n- [ ] make sure product stock is ok\n- [ ] sku has a specific meaning. should change it's use in the code\n- [ ] use the lookup_key for prices to relate them to a product\n- [ ] only list products that have stock\n\n[How to access query parameters in Netlify functions](https://flaviocopes.com/netlify-functions-query-parameters/)\n\n[Email receipts](https://stripe.com/docs/receipts)\n\n\u003e Stripe can automatically send email receipts after a successful payment, or when you refund one. This is done by providing an email address when making the API request, using the email address of a Customer object, or updating a PaymentIntent with a customer’s email address after checkout\n\u003e Receipts for payments created using your test API keys are not sent automatically. Instead, you can view or manually send a receipt using the Dashboard.\n\n------------------------------\n* could get products at build time instead of on page load. But that doesn't work because the stock needs to be up to date.\n* make the sucess/cancel pages\n----------------------------------\n\n## should maybe use fauna + netlify CMS like originally planned b/c stripe doesn't keep stock for us\n\non product create:\nnetlify CMS -\u003e ntl function -- create fauna doc\n\non Purchase:\nntl fn -\u003e stock -1 in fauna, purchase in stripe \nNeed to use some kind of listener on stripe to know when to -1 the stock\n\n## comerce.js\n3% transaction fee -- $2000/month sales\n[commerce.js](https://commercejs.com/)\n[commerce.js docs](https://commercejs.com/docs/) -- cart api\n[capture order](https://commercejs.com/docs/api/?javascript#capture-order)\n[get cart contents](https://commercejs.com/docs/api/?javascript#get-cart-contents)\n\n----------------------------------\n\n[Accept a payment](https://stripe.com/docs/payments/accept-a-payment#web)\n\nUse `elements` in the browser \u0026 `paymentIntent` on the server to make a payment. Use `paymentIntent.client_secret`\n\n---------------------------------\n\nAnd it's back to using the netlify CMS to create docs in fauna I guess.\n\n[Registering to CMS Events](https://www.netlifycms.org/docs/beta-features/#registering-to-cms-events)\n\n\u003e Supported events are `prePublish`, `postPublish`, `preUnpublish`, `postUnpublish`, `preSave` and `postSave`\n\n```js\nCMS.registerEventListener({\n    name: 'prePublish',\n    handler: function ({ author, entry }) {\n        var str = JSON.stringify({ author, data: entry.get('data') })\n        console.log('prepublish', str)\n    }\n});\n\n{\n  \"author\":{\"login\":\"nichoth@gmail.com\",\"name\":\"nichoth\"},\n  \"data\":{\n    \"name\":\"two\",\n    \"pic\":\"images/uploads/vag-eye.jpg\",\n    \"description\":\"woo two description\"\n  }\n}\n```\n\n----------------------------------------\n\n[Create, retrieve, update, and delete documents in FaunaDB](https://docs.fauna.com/fauna/current/tutorials/crud.html)\n\n[Quick start with FaunaDB](https://docs.fauna.com/fauna/current/start/cloud.html)\n\n[Create](https://docs.fauna.com/fauna/current/api/fql/functions/create)\n\n-------------------------------------\n\n[FaunaDB/Cookbook/Overview](https://docs.fauna.com/fauna/current/cookbook/)\n\u003e Reading or writing database definitions requires an admin key.\n```js\nvar adminClient = new faunadb.Client({\n  secret: adminKey\n})\n\nadminClient.query(\n  q.CreateDatabase({ name: 'annuvin' })\n)\n.then((ret) =\u003e console.log(ret))\n```\n\n[Create a document in a collection](https://docs.fauna.com/fauna/current/cookbook/#collection-create-document) -- spells example\n\n[Create a post](https://docs.fauna.com/fauna/current/tutorials/crud#post)\n\n[Building Serverless CRUD apps with Netlify Functions \u0026 FaunaDB](https://www.netlify.com/blog/2018/07/09/building-serverless-crud-apps-with-netlify-functions-faunadb/)\n\n\u003e If it’s a valid token issued by the Identity instance linked to the site, Netlify will add the user’s claims in a `context.clientContext.user` object.\nYou can use this object to add a little guard clause to the handler method in slack.js, blocking access for users who haven’t logged in\n[JAMstack architecture on Netlify: How Identity and Functions work together](https://www.netlify.com/blog/2018/03/29/jamstack-architecture-on-netlify-how-identity-and-functions-work-together/)\n\n\n------------------------------------------\n\nThur 6-18\nAuthenticate users before allowing product create URL call.\n\n[identity](https://www.netlifycms.org/docs/add-to-your-site/#authentication) -- example of event listeners on global identity\n\n---------------------------------------\n\n## today\n* [ ] automated tests\n    - [x] need to know which directory is served as root from tape-run\n    - [ ] write tests\n* [x] list the products\n* [ ] make a quantity field in the products\n\n---------------------------------------------\n\n* [x] list the products\n* [x] front-end can display products\n* [ ] write a test\n* [x] there is a slug for the product URL -- put it in DB\n  slug = URL\n  fetch('get-product', { body: { slug } })\n\n  or\n\n  --get them all in eleventy and make a separate page for each with the slug as path--\n\nNeed to do it at run time so stock is up to date\n\n[Building Serverless CRUD apps with Netlify Functions \u0026 FaunaDB](https://www.netlify.com/blog/2018/07/09/building-serverless-crud-apps-with-netlify-functions-faunadb/)\n\n[Create, retrieve, update, and delete documents in FaunaDB](https://docs.fauna.com/fauna/current/tutorials/crud.html#retrieve)\n\u003e You can query for posts with a specific title using the match function and the index we created earlier\n\n---------------------------------------\n\n\n* [ ] has a 404 page for missing slug\n* [x] make page for single product\n\n------------------------------------------\n\n* [x] use a separate html page for the product page. Without the logo\n* [x] css for single product page\n* [x] stripe purchase works\n\nhttps://stripe.com/docs/payments/accept-a-payment#web\n\n------------------------------------------\n6-28\n\n* [ ] should show error messages for bad payment\n* [ ] show a success page after payment\n* [ ] should show CMS error states\n* [ ] diff pages by genre. Need genre input in CMS\n* [ ] can \"tag\" things\n* [ ] shipping cost calculator\n* [x] can delete things\n* [x] description in CMS is optional\n* [x] localhost `identity` widget forwards to the right place\n* [x] pagination is ok in fauna query\n* [ ] create an order\n* [x] fix config.yml\n\n[Registering to CMS Events](https://www.netlifycms.org/docs/beta-features/#registering-to-cms-events) -- list of CMS events\n\n\u003e Supported events are prePublish, postPublish, preUnpublish, postUnpublish, preSave and postSave.\n\nhttps://github.com/netlify/netlify-cms/blob/2b46608f86d22c8ad34f75e396be7c34462d9e99/packages/netlify-cms-core/src/lib/registry.js#L6\n```js\nconst allowedEvents = [\n  'prePublish',\n  'postPublish',\n  'preUnpublish',\n  'postUnpublish',\n  'preSave',\n  'postSave',\n];\n```\n\n```js\n  console.log('slug', entry.get('slug'))\n```\n\nhttps://docs.fauna.com/fauna/current/tutorials/indexes/pagination.html#query-large\n\u003e The Paginate function defaults to returning up to 64 documents in a \"page\", which is a subset of the results.\n\nIf you use the github or google button on the identity login, it will redirect to the live site URL instead of localhost, but it works with the password.\n\n--------------------------\n\n[fauna ecommerce](https://docs.fauna.com/fauna/current/tutorials/ecommerce)\n[delete a post](https://docs.fauna.com/fauna/current/tutorials/crud.html#delete)\n[delete](https://docs.fauna.com/fauna/current/api/fql/functions/delete)\n\n----------------------------------\n\n[stripe webhook stuff](https://stripe.com/docs/payments/payment-intents/verifying-status#webhooks)\n\n\n---------------------------------\n\n## webhook test\nServe the functions locally:\n```\nnpm start\n```\n\n\nSetup the stripe webhook destination\n```\nstripe listen --forward-to localhost:54901/stripe-webhook\n```\n|^ You need to change the port to the right one (the one returned by `npm start`)\n|^ Need to get the secret that is returned from this and put it in env for\n    the function\n\n\nSend the webhook request (it has the right body \u0026 headers automatically)\n```\n$ stripe trigger payment_intent.succeeded\n```\n\n```js\nserveFunctions({\n    functionsDir: path.join(__dirname, '..', 'functions'),\n    port: 9090\n});\n```\n\nhttps://github.com/netlify/cli/issues/566\nhttps://stripe.com/docs/stripe-cli/webhooks#forward-events\nhttps://stripe.com/docs/api/events/types\nhttps://stripe.com/docs/api/payment_intents/object\nhttps://stripe.com/docs/webhooks/test?lang=node\nhttps://stripe.com/docs/webhooks/build?lang=node\n\n\n\n```\nstripeEv {\n  id: 'evt_1H0yxlBnqQbRlIvQOcDabxh4',\n  object: 'event',\n  api_version: '2020-03-02',\n  created: 1593821435,\n  data: {\n    object: {\n      id: 'pi_1H0yxjBnqQbRlIvQWlHcTMCr',\n      object: 'payment_intent',\n      amount: 2000,\n      amount_capturable: 0,\n      amount_received: 0,\n      application: null,\n      application_fee_amount: null,\n      canceled_at: null,\n      cancellation_reason: null,\n      capture_method: 'automatic',\n      charges: [Object],\n      client_secret: 'pi_1H0yxjBnqQbRlIvQWlHcTMCr_secret_mXGt3NGtYkEjDEqGrOVE2QUQk',\n      confirmation_method: 'automatic',\n      created: 1593821435,\n      currency: 'usd',\n      customer: null,\n      description: '(created by Stripe CLI)',\n      invoice: null,\n      last_payment_error: null,\n      livemode: false,\n      metadata: {},\n      next_action: null,\n      on_behalf_of: null,\n      payment_method: null,\n      payment_method_options: [Object],\n      payment_method_types: [Array],\n      receipt_email: null,\n      review: null,\n      setup_future_usage: null,\n      shipping: [Object],\n      source: null,\n      statement_descriptor: null,\n      statement_descriptor_suffix: null,\n      status: 'requires_payment_method',\n      transfer_data: null,\n      transfer_group: null\n    }\n  },\n  livemode: false,\n  pending_webhooks: 2,\n  request: { id: 'req_JaobDjxa7aP0tW', idempotency_key: null },\n  type: 'payment_intent.created'\n}\n\n.data {\n  object: {\n    id: 'pi_1H0yxjBnqQbRlIvQWlHcTMCr',\n    object: 'payment_intent',\n    amount: 2000,\n    amount_capturable: 0,\n    amount_received: 0,\n    application: null,\n    application_fee_amount: null,\n    canceled_at: null,\n    cancellation_reason: null,\n    capture_method: 'automatic',\n    charges: {\n      object: 'list',\n      data: [],\n      has_more: false,\n      total_count: 0,\n      url: '/v1/charges?payment_intent=pi_1H0yxjBnqQbRlIvQWlHcTMCr'\n    },\n    client_secret: 'pi_1H0yxjBnqQbRlIvQWlHcTMCr_secret_mXGt3NGtYkEjDEqGrOVE2QUQk',\n    confirmation_method: 'automatic',\n    created: 1593821435,\n    currency: 'usd',\n    customer: null,\n    description: '(created by Stripe CLI)',\n    invoice: null,\n    last_payment_error: null,\n    livemode: false,\n    metadata: {},\n    next_action: null,\n    on_behalf_of: null,\n    payment_method: null,\n    payment_method_options: { card: [Object] },\n    payment_method_types: [ 'card' ],\n    receipt_email: null,\n    review: null,\n    setup_future_usage: null,\n    shipping: {\n      address: [Object],\n      carrier: null,\n      name: 'Jenny Rosen',\n      phone: null,\n      tracking_number: null\n    },\n    source: null,\n    statement_descriptor: null,\n    statement_descriptor_suffix: null,\n    status: 'requires_payment_method',\n    transfer_data: null,\n    transfer_group: null\n  }\n}\n```\n\n\n-----------------------------------------\n\n[Elements validates user input as it is typed. To help your customers catch mistakes, listen to change events on the card Element and display any errors](https://stripe.com/docs/payments/accept-a-payment?lang=node)\n\n\n\u003e Use the Dashboard, a custom webhook, or a third-party plugin to handle *post-payment events* like sending an order confirmation email to your customer, logging the sale in a database, or starting a shipping workflow.\n\n^ https://stripe.com/docs/payments/checkout/accept-a-payment?lang=node\n\n[metadata](https://stripe.com/docs/payments/payment-intents#storing-information-in-metadata)\n\n\n--------------------------------\n\nhttps://stripe.com/docs/payments/checkout/accept-a-payment\n\n\n\u003e To acknowledge receipt of an event, your endpoint must return a 2xx HTTP status code to Stripe. All response codes outside this range, including 3xx codes, indicate to Stripe that you did not receive the event.\nhttps://stripe.com/docs/webhooks/build?lang=node#return-a-2xx-status-code-quickly\n\n[Shipping address collection](https://stripe.com/docs/payments/checkout/customization?lang=node#shipping-address-collection)\n\n\n----------------------------------------------------\n\n\n## stripe webhook payload\n```\n{\n  \"id\": \"evt_1H0yxlBnqQbRlIvQHy2QnROF\",\n  \"object\": \"event\",\n  \"api_version\": \"2020-03-02\",\n  \"created\": 1593821436,\n  \"data\": {\n    \"object\": {\n      \"id\": \"pi_1H0yxjBnqQbRlIvQWlHcTMCr\",\n      \"object\": \"payment_intent\",\n      \"amount\": 2000,\n      \"amount_capturable\": 0,\n      \"amount_received\": 2000,\n      \"application\": null,\n      \"application_fee_amount\": null,\n      \"canceled_at\": null,\n      \"cancellation_reason\": null,\n      \"capture_method\": \"automatic\",\n      \"charges\": {\n        \"object\": \"list\",\n        \"data\": [\n          {\n            \"id\": \"ch_1H0yxkBnqQbRlIvQ4E6pnq5s\",\n            \"object\": \"charge\",\n            \"amount\": 2000,\n            \"amount_refunded\": 0,\n            \"application\": null,\n            \"application_fee\": null,\n            \"application_fee_amount\": null,\n            \"balance_transaction\": \"txn_1H0yxkBnqQbRlIvQS1apGxi2\",\n            \"billing_details\": {\n              \"address\": {\n                \"city\": null,\n                \"country\": null,\n                \"line1\": null,\n                \"line2\": null,\n                \"postal_code\": null,\n                \"state\": null\n              },\n              \"email\": null,\n              \"name\": null,\n              \"phone\": null\n            },\n            \"calculated_statement_descriptor\": \"Stripe\",\n            \"captured\": true,\n            \"created\": 1593821436,\n            \"currency\": \"usd\",\n            \"customer\": null,\n            \"description\": \"(created by Stripe CLI)\",\n            \"destination\": null,\n            \"dispute\": null,\n            \"disputed\": false,\n            \"failure_code\": null,\n            \"failure_message\": null,\n            \"fraud_details\": {\n            },\n            \"invoice\": null,\n            \"livemode\": false,\n            \"metadata\": {\n            },\n            \"on_behalf_of\": null,\n            \"order\": null,\n            \"outcome\": {\n              \"network_status\": \"approved_by_network\",\n              \"reason\": null,\n              \"risk_level\": \"normal\",\n              \"risk_score\": 7,\n              \"seller_message\": \"Payment complete.\",\n              \"type\": \"authorized\"\n            },\n            \"paid\": true,\n            \"payment_intent\": \"pi_1H0yxjBnqQbRlIvQWlHcTMCr\",\n            \"payment_method\": \"pm_1H0yxjBnqQbRlIvQckXyRocI\",\n            \"payment_method_details\": {\n              \"card\": {\n                \"brand\": \"visa\",\n                \"checks\": {\n                  \"address_line1_check\": null,\n                  \"address_postal_code_check\": null,\n                  \"cvc_check\": null\n                },\n                \"country\": \"US\",\n                \"exp_month\": 7,\n                \"exp_year\": 2021,\n                \"fingerprint\": \"7vCGW0oukBtkxGQE\",\n                \"funding\": \"credit\",\n                \"installments\": null,\n                \"last4\": \"4242\",\n                \"network\": \"visa\",\n                \"three_d_secure\": null,\n                \"wallet\": null\n              },\n              \"type\": \"card\"\n            },\n            \"receipt_email\": null,\n            \"receipt_number\": null,\n            \"receipt_url\": \"https://pay.stripe.com/receipts/acct_1GlJDeBnqQbRlIvQ/ch_1H0yxkBnqQbRlIvQ4E6pnq5s/rcpt_Ha9G7PYQqEdXtO2earxkQhRWF14fXF6\",\n            \"refunded\": false,\n            \"refunds\": {\n              \"object\": \"list\",\n              \"data\": [\n              ],\n              \"has_more\": false,\n              \"total_count\": 0,\n              \"url\": \"/v1/charges/ch_1H0yxkBnqQbRlIvQ4E6pnq5s/refunds\"\n            },\n            \"review\": null,\n            \"shipping\": {\n              \"address\": {\n                \"city\": \"San Francisco\",\n                \"country\": \"US\",\n                \"line1\": \"510 Townsend St\",\n                \"line2\": null,\n                \"postal_code\": \"94103\",\n                \"state\": \"CA\"\n              },\n              \"carrier\": null,\n              \"name\": \"Jenny Rosen\",\n              \"phone\": null,\n              \"tracking_number\": null\n            },\n            \"source\": null,\n            \"source_transfer\": null,\n            \"statement_descriptor\": null,\n            \"statement_descriptor_suffix\": null,\n            \"status\": \"succeeded\",\n            \"transfer_data\": null,\n            \"transfer_group\": null\n          }\n        ],\n        \"has_more\": false,\n        \"total_count\": 1,\n        \"url\": \"/v1/charges?payment_intent=pi_1H0yxjBnqQbRlIvQWlHcTMCr\"\n      },\n      \"client_secret\": \"pi_1H0yxjBnqQbRlIvQWlHcTMCr_secret_mXGt3NGtYkEjDEqGrOVE2QUQk\",\n      \"confirmation_method\": \"automatic\",\n      \"created\": 1593821435,\n      \"currency\": \"usd\",\n      \"customer\": null,\n      \"description\": \"(created by Stripe CLI)\",\n      \"invoice\": null,\n      \"last_payment_error\": null,\n      \"livemode\": false,\n      \"metadata\": {\n      },\n      \"next_action\": null,\n      \"on_behalf_of\": null,\n      \"payment_method\": \"pm_1H0yxjBnqQbRlIvQckXyRocI\",\n      \"payment_method_options\": {\n        \"card\": {\n          \"installments\": null,\n          \"network\": null,\n          \"request_three_d_secure\": \"automatic\"\n        }\n      },\n      \"payment_method_types\": [\n        \"card\"\n      ],\n      \"receipt_email\": null,\n      \"review\": null,\n      \"setup_future_usage\": null,\n      \"shipping\": {\n        \"address\": {\n          \"city\": \"San Francisco\",\n          \"country\": \"US\",\n          \"line1\": \"510 Townsend St\",\n          \"line2\": null,\n          \"postal_code\": \"94103\",\n          \"state\": \"CA\"\n        },\n        \"carrier\": null,\n        \"name\": \"Jenny Rosen\",\n        \"phone\": null,\n        \"tracking_number\": null\n      },\n      \"source\": null,\n      \"statement_descriptor\": null,\n      \"statement_descriptor_suffix\": null,\n      \"status\": \"succeeded\",\n      \"transfer_data\": null,\n      \"transfer_group\": null\n    }\n  },\n  \"livemode\": false,\n  \"pending_webhooks\": 1,\n  \"request\": {\n    \"id\": \"req_JaobDjxa7aP0tW\",\n    \"idempotency_key\": null\n  },\n  \"type\": \"payment_intent.succeeded\"\n}\n```\n\n## e2e test\n```\nnpm start\n```\nVisit localhost:8888/\u003cproduct\u003e.\nSubmit the test CC number. \n\n* webhook works -- check in stripe/netlify dashboard\n* success page shows -- need to redirect/re-render in client side\n* product stock goes -1 -- do this in `create-order` function\n\n\n-------------------------------\n\n### Decrement operation\nhttps://stackoverflow.com/questions/56894199/how-to-increment-value-in-faunadb-using-javascript-and-serverside-functions\n\n---------------------------------------\n\n[From Static Sites To End User JAMstack Apps With FaunaDB](https://www.smashingmagazine.com/2020/06/static-sites-jamstack-apps-faunadb/)\n\n[E-commerce with fauna tutorial](https://docs.fauna.com/fauna/current/tutorials/ecommerce.html)\n\n\n--------------------------------------------\n\n[synchronous, server side payments](https://stripe.com/docs/payments/accept-a-payment-synchronously)\n\n[checkout version](https://stripe.com/docs/payments/checkout/accept-a-payment) -- Where you forward the customer to the stripe page. Note the webhook happens before the response in the picture diagram. This is not ideal because the stock decrment/purchase operation are not atomic. You have to buy the thing, then stock is decremented later.\n\n[payment intent](https://stripe.com/docs/payments/payment-intents#best-practices) -- idenpotency key, etc\n\n\u003e Idempotency keys are sent in the Idempotency-Key header, and you should use them for all POST requests to Stripe’s API.\n[idempotency key](https://stripe.com/docs/error-handling#sending-idempotency-keys)\n\n\nthe plan\nDo the 'synchronous' type of payment process, which happens server-side, and create an `order` record as well. See [accept-a-payment-synchronously](https://stripe.com/docs/payments/accept-a-payment-synchronously)\n\n\nUse `stripe.createPaymentMethod` on the client\n\nSend the payment method ID to the server where it pays for things\n```js\nfetch('/pay', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        payment_method_id: result.paymentMethod.id,\n      })\n    }).then(function(result) {\n      // Handle server response (see Step 4)\n      result.json().then(function(json) {\n        handleServerResponse(json);\n      })\n    });\n```\n\nOn the server, create and confirm the paymentIntent\n```js\napp.post('/pay', async (request, response) =\u003e {\n    let intent;\n    if (request.body.payment_method_id) {\n      // Create the PaymentIntent\n      intent = await stripe.paymentIntents.create({\n        payment_method: request.body.payment_method_id,\n        amount: 1099,\n        currency: 'usd',\n        confirmation_method: 'manual',\n        confirm: true\n      });\n```\n\nFirst check/decrement the stock, then if it's ok, create an `order` record. Then call stripe.pay and set `confirm: true` on the paymentIntent call\n\nThen when you get the webhook sucess for the payment, mark the order as `paid`, and start shipping things\n\n\u003e Stripe sends a payment_intent.succeeded event when the payment completes. Use the Dashboard, a custom webhook, or a partner solution to receive these events and run actions, like sending an order confirmation email to your customer, logging the sale in a database, or starting a shipping workflow.\n\n\n\n-----------------------------------------\n\n\n[server side confirm payment](https://stripe.com/docs/api/payment_intents/confirm)\n\n\n---------------------------------\n\n\n\n\n## [ecommerce fauna example](https://docs.fauna.com/fauna/current/tutorials/ecommerce#function)\n\nIf you call this with a non-existant slug, it will return an error and say \"Calling the function resulted in an error.\"\n\nExanmple that works:\n```js\nCall(\n  Function(\"submit_order\"),\n    [\n      [Object({\n        \"slug\": \"aaaaaaaaaaa\",\n        \"quantity\": 1\n      })]\n    ]\n)\n```\nres: \n```js\n{\n  ref: Ref(Collection(\"orders\"), \"271505982105846280\"),\n  ts: 1595187131910000,\n  data: {\n    line: [\n      {\n        product: Ref(Collection(\"products\"), \"269177027692593672\"),\n        quantity: 1,\n        price: 10\n      }\n    ],\n    status: \"new\",\n    creationDate: Time(\"2020-07-19T19:32:11.640455Z\"),\n    shipAddress: \"123 street\",\n    creditCard: \"4425\"\n  }\n}\n```\n\n-------------------------------------------------\n\n[form validation](https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation)\n[validation tips](https://daverupert.com/2017/11/happier-html5-forms/)\n\n\n-----------------------------------------------------------------\n\n\n[metadata](https://stripe.com/docs/api/payment_methods/object#payment_method_object-metadata)\n\n\n\n-----------------------------------------------------------------\n\n## testing the 'buy' page\n\nneed to use the `buy-things` page\nreturn something from the `buy` page js\n\n\n\n---------------------------------------------------\n\nHow to get the iframe to work\n[Testing Stripe Elements with Cypress](https://medium.com/@michabahr/testing-stripe-elements-with-cypress-5a2fc17ab27b)\n\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnichoth%2Fwhammy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnichoth%2Fwhammy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnichoth%2Fwhammy/lists"}