Skip to content

nichoth/whammy

Repository files navigation

whammy

Trying things with netlify & stripe

Start the netlify local dev server, watch css and js files

npm start

Build everything, the same way it will be deployed

npm run build

Tests for the buy things page

npm test

Open the cypress test gui

npm run test:open

test cards

4242424242424242

Succeeds and immediately processes the payment.

4000000000003220

3D Secure 2 authentication must be completed for a successful payment.

4000000000009995

Always fails with a decline code of insufficient_funds.

4000002760003184

A test card number that requires authentication.


Products and Prices

If you only have a few products, you should create and manage them in the Dashboard

List all products

require('dotenv').config()
var stripe = require('stripe')(process.env.STRIPE_SECRET);

stripe.products.list({ limit: 3 }, function (err, products) {
    // asynchronously called
});

Create a customer

Before billing a customer, you need to create a Customer object that you can configure with a name, email, and payment method.

// Set your secret key. Remember to switch to your live secret key in production!
// See your keys here: https://dashboard.stripe.com/account/apikeys
const stripe = require('stripe')('sk_test_51GrU9fGmvbUUvDLHxIdVkGP7SsJv9Re4AY6gJ4E9rR55pEIozVyX0BF2H8CO2mpYuZg3eDr4ftjjmTD9GNKsJoMk00wn6cXykX');

const customer = await stripe.customers.create({
  email: 'jenny.rosen@example.com',
  payment_method: 'pm_card_visa',
  invoice_settings: {
    default_payment_method: 'pm_card_visa',
  },
});

checkout


TODO today

for the checkout

var stripe = require('stripe')('sk_test_51GrU9fGmvbUUvDLHxIdVkGP7SsJv9Re4AY6gJ4E9rR55pEIozVyX0BF2H8CO2mpYuZg3eDr4ftjjmTD9GNKsJoMk00wn6cXykX');

stripe.products.retrieve('prod_HQTNO4cLeDwzDX', function (err, product) {
    // asynchronously called
});

todo

  • redirect to a good place after purchase
  • make sure product stock is ok
  • sku has a specific meaning. should change it's use in the code
  • use the lookup_key for prices to relate them to a product
  • only list products that have stock

How to access query parameters in Netlify functions

Email receipts

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 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.


  • 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.
  • make the sucess/cancel pages

should maybe use fauna + netlify CMS like originally planned b/c stripe doesn't keep stock for us

on product create: netlify CMS -> ntl function -- create fauna doc

on Purchase: ntl fn -> stock -1 in fauna, purchase in stripe Need to use some kind of listener on stripe to know when to -1 the stock

comerce.js

3% transaction fee -- $2000/month sales commerce.js commerce.js docs -- cart api capture order get cart contents


Accept a payment

Use elements in the browser & paymentIntent on the server to make a payment. Use paymentIntent.client_secret


And it's back to using the netlify CMS to create docs in fauna I guess.

Registering to CMS Events

Supported events are prePublish, postPublish, preUnpublish, postUnpublish, preSave and postSave

CMS.registerEventListener({
    name: 'prePublish',
    handler: function ({ author, entry }) {
        var str = JSON.stringify({ author, data: entry.get('data') })
        console.log('prepublish', str)
    }
});

{
  "author":{"login":"nichoth@gmail.com","name":"nichoth"},
  "data":{
    "name":"two",
    "pic":"images/uploads/vag-eye.jpg",
    "description":"woo two description"
  }
}

Create, retrieve, update, and delete documents in FaunaDB

Quick start with FaunaDB

Create


FaunaDB/Cookbook/Overview

Reading or writing database definitions requires an admin key.

var adminClient = new faunadb.Client({
  secret: adminKey
})

adminClient.query(
  q.CreateDatabase({ name: 'annuvin' })
)
.then((ret) => console.log(ret))

Create a document in a collection -- spells example

Create a post

Building Serverless CRUD apps with Netlify Functions & FaunaDB

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. You 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 JAMstack architecture on Netlify: How Identity and Functions work together


Thur 6-18 Authenticate users before allowing product create URL call.

identity -- example of event listeners on global identity


today

  • automated tests
    • need to know which directory is served as root from tape-run
    • write tests
  • list the products
  • make a quantity field in the products

  • list the products

  • front-end can display products

  • write a test

  • there is a slug for the product URL -- put it in DB slug = URL fetch('get-product', { body: { slug } })

    or

    --get them all in eleventy and make a separate page for each with the slug as path--

Need to do it at run time so stock is up to date

Building Serverless CRUD apps with Netlify Functions & FaunaDB

Create, retrieve, update, and delete documents in FaunaDB

You can query for posts with a specific title using the match function and the index we created earlier


  • has a 404 page for missing slug
  • make page for single product

  • use a separate html page for the product page. Without the logo
  • css for single product page
  • stripe purchase works

https://stripe.com/docs/payments/accept-a-payment#web


6-28

  • should show error messages for bad payment
  • show a success page after payment
  • should show CMS error states
  • diff pages by genre. Need genre input in CMS
  • can "tag" things
  • shipping cost calculator
  • can delete things
  • description in CMS is optional
  • localhost identity widget forwards to the right place
  • pagination is ok in fauna query
  • create an order
  • fix config.yml

Registering to CMS Events -- list of CMS events

Supported events are prePublish, postPublish, preUnpublish, postUnpublish, preSave and postSave.

https://github.com/netlify/netlify-cms/blob/2b46608f86d22c8ad34f75e396be7c34462d9e99/packages/netlify-cms-core/src/lib/registry.js#L6

const allowedEvents = [
  'prePublish',
  'postPublish',
  'preUnpublish',
  'postUnpublish',
  'preSave',
  'postSave',
];
  console.log('slug', entry.get('slug'))

https://docs.fauna.com/fauna/current/tutorials/indexes/pagination.html#query-large

The Paginate function defaults to returning up to 64 documents in a "page", which is a subset of the results.

If 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.


fauna ecommerce delete a post delete


stripe webhook stuff


webhook test

Serve the functions locally:

npm start

Setup the stripe webhook destination

stripe listen --forward-to localhost:54901/stripe-webhook

|^ You need to change the port to the right one (the one returned by npm start) |^ Need to get the secret that is returned from this and put it in env for the function

Send the webhook request (it has the right body & headers automatically)

$ stripe trigger payment_intent.succeeded
serveFunctions({
    functionsDir: path.join(__dirname, '..', 'functions'),
    port: 9090
});

netlify/cli#566 https://stripe.com/docs/stripe-cli/webhooks#forward-events https://stripe.com/docs/api/events/types https://stripe.com/docs/api/payment_intents/object https://stripe.com/docs/webhooks/test?lang=node https://stripe.com/docs/webhooks/build?lang=node

stripeEv {
  id: 'evt_1H0yxlBnqQbRlIvQOcDabxh4',
  object: 'event',
  api_version: '2020-03-02',
  created: 1593821435,
  data: {
    object: {
      id: 'pi_1H0yxjBnqQbRlIvQWlHcTMCr',
      object: 'payment_intent',
      amount: 2000,
      amount_capturable: 0,
      amount_received: 0,
      application: null,
      application_fee_amount: null,
      canceled_at: null,
      cancellation_reason: null,
      capture_method: 'automatic',
      charges: [Object],
      client_secret: 'pi_1H0yxjBnqQbRlIvQWlHcTMCr_secret_mXGt3NGtYkEjDEqGrOVE2QUQk',
      confirmation_method: 'automatic',
      created: 1593821435,
      currency: 'usd',
      customer: null,
      description: '(created by Stripe CLI)',
      invoice: null,
      last_payment_error: null,
      livemode: false,
      metadata: {},
      next_action: null,
      on_behalf_of: null,
      payment_method: null,
      payment_method_options: [Object],
      payment_method_types: [Array],
      receipt_email: null,
      review: null,
      setup_future_usage: null,
      shipping: [Object],
      source: null,
      statement_descriptor: null,
      statement_descriptor_suffix: null,
      status: 'requires_payment_method',
      transfer_data: null,
      transfer_group: null
    }
  },
  livemode: false,
  pending_webhooks: 2,
  request: { id: 'req_JaobDjxa7aP0tW', idempotency_key: null },
  type: 'payment_intent.created'
}

.data {
  object: {
    id: 'pi_1H0yxjBnqQbRlIvQWlHcTMCr',
    object: 'payment_intent',
    amount: 2000,
    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: [],
      has_more: false,
      total_count: 0,
      url: '/v1/charges?payment_intent=pi_1H0yxjBnqQbRlIvQWlHcTMCr'
    },
    client_secret: 'pi_1H0yxjBnqQbRlIvQWlHcTMCr_secret_mXGt3NGtYkEjDEqGrOVE2QUQk',
    confirmation_method: 'automatic',
    created: 1593821435,
    currency: 'usd',
    customer: null,
    description: '(created by Stripe CLI)',
    invoice: null,
    last_payment_error: null,
    livemode: false,
    metadata: {},
    next_action: null,
    on_behalf_of: null,
    payment_method: null,
    payment_method_options: { card: [Object] },
    payment_method_types: [ 'card' ],
    receipt_email: null,
    review: null,
    setup_future_usage: null,
    shipping: {
      address: [Object],
      carrier: null,
      name: 'Jenny Rosen',
      phone: null,
      tracking_number: null
    },
    source: null,
    statement_descriptor: null,
    statement_descriptor_suffix: null,
    status: 'requires_payment_method',
    transfer_data: null,
    transfer_group: null
  }
}

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

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.

^ https://stripe.com/docs/payments/checkout/accept-a-payment?lang=node

metadata


https://stripe.com/docs/payments/checkout/accept-a-payment

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. https://stripe.com/docs/webhooks/build?lang=node#return-a-2xx-status-code-quickly

Shipping address collection


stripe webhook payload

{
  "id": "evt_1H0yxlBnqQbRlIvQHy2QnROF",
  "object": "event",
  "api_version": "2020-03-02",
  "created": 1593821436,
  "data": {
    "object": {
      "id": "pi_1H0yxjBnqQbRlIvQWlHcTMCr",
      "object": "payment_intent",
      "amount": 2000,
      "amount_capturable": 0,
      "amount_received": 2000,
      "application": null,
      "application_fee_amount": null,
      "canceled_at": null,
      "cancellation_reason": null,
      "capture_method": "automatic",
      "charges": {
        "object": "list",
        "data": [
          {
            "id": "ch_1H0yxkBnqQbRlIvQ4E6pnq5s",
            "object": "charge",
            "amount": 2000,
            "amount_refunded": 0,
            "application": null,
            "application_fee": null,
            "application_fee_amount": null,
            "balance_transaction": "txn_1H0yxkBnqQbRlIvQS1apGxi2",
            "billing_details": {
              "address": {
                "city": null,
                "country": null,
                "line1": null,
                "line2": null,
                "postal_code": null,
                "state": null
              },
              "email": null,
              "name": null,
              "phone": null
            },
            "calculated_statement_descriptor": "Stripe",
            "captured": true,
            "created": 1593821436,
            "currency": "usd",
            "customer": null,
            "description": "(created by Stripe CLI)",
            "destination": null,
            "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": 7,
              "seller_message": "Payment complete.",
              "type": "authorized"
            },
            "paid": true,
            "payment_intent": "pi_1H0yxjBnqQbRlIvQWlHcTMCr",
            "payment_method": "pm_1H0yxjBnqQbRlIvQckXyRocI",
            "payment_method_details": {
              "card": {
                "brand": "visa",
                "checks": {
                  "address_line1_check": null,
                  "address_postal_code_check": null,
                  "cvc_check": null
                },
                "country": "US",
                "exp_month": 7,
                "exp_year": 2021,
                "fingerprint": "7vCGW0oukBtkxGQE",
                "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_1GlJDeBnqQbRlIvQ/ch_1H0yxkBnqQbRlIvQ4E6pnq5s/rcpt_Ha9G7PYQqEdXtO2earxkQhRWF14fXF6",
            "refunded": false,
            "refunds": {
              "object": "list",
              "data": [
              ],
              "has_more": false,
              "total_count": 0,
              "url": "/v1/charges/ch_1H0yxkBnqQbRlIvQ4E6pnq5s/refunds"
            },
            "review": null,
            "shipping": {
              "address": {
                "city": "San Francisco",
                "country": "US",
                "line1": "510 Townsend St",
                "line2": null,
                "postal_code": "94103",
                "state": "CA"
              },
              "carrier": null,
              "name": "Jenny Rosen",
              "phone": null,
              "tracking_number": null
            },
            "source": null,
            "source_transfer": null,
            "statement_descriptor": null,
            "statement_descriptor_suffix": null,
            "status": "succeeded",
            "transfer_data": null,
            "transfer_group": null
          }
        ],
        "has_more": false,
        "total_count": 1,
        "url": "/v1/charges?payment_intent=pi_1H0yxjBnqQbRlIvQWlHcTMCr"
      },
      "client_secret": "pi_1H0yxjBnqQbRlIvQWlHcTMCr_secret_mXGt3NGtYkEjDEqGrOVE2QUQk",
      "confirmation_method": "automatic",
      "created": 1593821435,
      "currency": "usd",
      "customer": null,
      "description": "(created by Stripe CLI)",
      "invoice": null,
      "last_payment_error": null,
      "livemode": false,
      "metadata": {
      },
      "next_action": null,
      "on_behalf_of": null,
      "payment_method": "pm_1H0yxjBnqQbRlIvQckXyRocI",
      "payment_method_options": {
        "card": {
          "installments": null,
          "network": null,
          "request_three_d_secure": "automatic"
        }
      },
      "payment_method_types": [
        "card"
      ],
      "receipt_email": null,
      "review": null,
      "setup_future_usage": null,
      "shipping": {
        "address": {
          "city": "San Francisco",
          "country": "US",
          "line1": "510 Townsend St",
          "line2": null,
          "postal_code": "94103",
          "state": "CA"
        },
        "carrier": null,
        "name": "Jenny Rosen",
        "phone": null,
        "tracking_number": null
      },
      "source": null,
      "statement_descriptor": null,
      "statement_descriptor_suffix": null,
      "status": "succeeded",
      "transfer_data": null,
      "transfer_group": null
    }
  },
  "livemode": false,
  "pending_webhooks": 1,
  "request": {
    "id": "req_JaobDjxa7aP0tW",
    "idempotency_key": null
  },
  "type": "payment_intent.succeeded"
}

e2e test

npm start

Visit localhost:8888/. Submit the test CC number.

  • webhook works -- check in stripe/netlify dashboard
  • success page shows -- need to redirect/re-render in client side
  • product stock goes -1 -- do this in create-order function

Decrement operation

https://stackoverflow.com/questions/56894199/how-to-increment-value-in-faunadb-using-javascript-and-serverside-functions


From Static Sites To End User JAMstack Apps With FaunaDB

E-commerce with fauna tutorial


synchronous, server side payments

checkout version -- 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.

payment intent -- idenpotency key, etc

Idempotency keys are sent in the Idempotency-Key header, and you should use them for all POST requests to Stripe’s API. idempotency key

the plan Do the 'synchronous' type of payment process, which happens server-side, and create an order record as well. See accept-a-payment-synchronously

Use stripe.createPaymentMethod on the client

Send the payment method ID to the server where it pays for things

fetch('/pay', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        payment_method_id: result.paymentMethod.id,
      })
    }).then(function(result) {
      // Handle server response (see Step 4)
      result.json().then(function(json) {
        handleServerResponse(json);
      })
    });

On the server, create and confirm the paymentIntent

app.post('/pay', async (request, response) => {
    let intent;
    if (request.body.payment_method_id) {
      // Create the PaymentIntent
      intent = await stripe.paymentIntents.create({
        payment_method: request.body.payment_method_id,
        amount: 1099,
        currency: 'usd',
        confirmation_method: 'manual',
        confirm: true
      });

First 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

Then when you get the webhook sucess for the payment, mark the order as paid, and start shipping things

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.


server side confirm payment


If you call this with a non-existant slug, it will return an error and say "Calling the function resulted in an error."

Exanmple that works:

Call(
  Function("submit_order"),
    [
      [Object({
        "slug": "aaaaaaaaaaa",
        "quantity": 1
      })]
    ]
)

res:

{
  ref: Ref(Collection("orders"), "271505982105846280"),
  ts: 1595187131910000,
  data: {
    line: [
      {
        product: Ref(Collection("products"), "269177027692593672"),
        quantity: 1,
        price: 10
      }
    ],
    status: "new",
    creationDate: Time("2020-07-19T19:32:11.640455Z"),
    shipAddress: "123 street",
    creditCard: "4425"
  }
}

form validation validation tips


metadata


testing the 'buy' page

need to use the buy-things page return something from the buy page js


How to get the iframe to work Testing Stripe Elements with Cypress