jasboys / sycpiano

Pianist Sean Chen's Website
MIT License
0 stars 1 forks source link

[PLAN] Shop #235

Open kamiyo opened 5 years ago

kamiyo commented 5 years ago

I thought I would create a plan for implementing the shop. After reading the Stripe APIs and thinking about how to do it this is what I came up with.

Few considerations:

  1. Be simple
  2. Ease of implementation
  3. Future-proof API

With that in mind, here are the things to do

Method A

Here's the shop flow:

  1. Fetch SKUs from Stripe to display on Shop (already implemented)
  2. Fetch cart from localStorage if exists
  3. Store cart in reducer (or initialize empty)
  4. For every cart update (add item or delete item) update reducer representation, and sync localStorage
  5. Cart is displayed as a side panel on desktop and drawer on mobile. Subtotal calculated on client for now.
  6. Require email field before checkout button is activated.
  7. On checkout click, send the SKUs and email to server. (Show loading icon while waiting)
  8. On the server, check if Customer exists with email address. If not, send an API request to Stripe to create one.
  9. Still on server, create a Session with line_items info populated from SKU information (from Stripe), the Customer ID, the customer email in payment_intent_data.receipt_email, and the actual skus as payment_intent_data.metadata in a hashmap:
    sku0: '{A_SKU}'
    sku1: '{ANOTHER_SKU}'
  10. Return the created Session ID to the client
  11. On client, call stripe.redirectToCheckout with the received session ID and the correct successUrl and cancelUrl.
  12. Customer fills out information and submits on Stripe Checkout page
  13. On the server, listen for checkout.session.completed webhook.
    1. Async fulfill by sending email with the correct PDF attachments using Nodemailer.
    2. Return any 2xx code.
  14. Customer is redirected to a payment success page if webhook returns 2xx code, which displays something along the lines of "Payment successful. PDFs will be sent by email from seanchen@seanchenpiano.com". Redirect back to shop page if cancelled (these urls are filled out in the Session).

Here's the flow to retrieve previously purchased pdfs (not efficient, but since we're not tracking customer IDs to email:

  1. Resend PDF's page - enter email in field.
  2. On submit, send email address to server.
  3. Fetch all customers with the given email address. For each CustomerID (should only be one, but who knows), fetch all PaymentIntents with customer=CustomerID. Filter by status==='succeeded' and reduce the metadata SKUs into an accumulating array. After all this is done, you should have an array with the SKUs to fetch; Fetch the list of SKUs from Stripe to get the filename (is this stored in the metadata?).
  4. Send email with the files attached.

Method B

Need Customer and Product tables, with each Customer having many Products. Customer's fields should have ID (we'll just use the Stripe generated ID), email, and hasMany association to Products. Products should have fields with ID, name, description, price, currency, image, filename.

Differences bolded.

Here's the shop flow:

  1. Fetch Products from DB to display on Shop
  2. Fetch cart from localStorage if exists
  3. Store cart in reducer (or initialize empty)
  4. For every cart update (add item or delete item) update reducer representation, and sync localStorage
  5. Cart is displayed as a side panel on desktop and drawer on mobile. Subtotal calculated on client for now.
  6. Require email field before checkout button is activated.
  7. On checkout click, send the Product IDs and email to server. (Show loading icon while waiting)
  8. On the server, check if Customer exists with email address (internally). If not, send an API request to Stripe to create one, then create one in internal DB with the same ID.
  9. Still on server, create a Session with line_items info populated from Product information (from DB), the Customer ID, the customer email in payment_intent_data.receipt_email, and the actual Product IDs as payment_intent_data.metadata in a hashmap:
    id0: '{A_PROD_ID}'
    id1: '{ANOTHER_PROD_ID}'
  10. Return the created Session ID to the client
  11. On client, call stripe.redirectToCheckout with the received session ID and the correct successUrl and cancelUrl.
  12. Customer fills out information and submits on Stripe Checkout page
  13. On the server, listen for checkout.session.completed webhook.
    1. Async fulfill by sending email with the correct PDF attachments using Nodemailer.
    2. Get CustomerID from the payload: session.customer, and the Products bought: session.payment_intent.metadata. In internal DB, associate Customer with the bought Products.
    3. Return any 2xx code.
  14. Customer is redirected to a payment success page if webhook returns 2xx code, which displays something along the lines of "Payment successful. PDFs will be sent by email from seanchen@seanchenpiano.com". Redirect back to shop page if cancelled (these urls are filled out in the Session).

Here's the flow to retrieve previously purchased pdfs (not efficient, but since we're not tracking customer IDs to email:

  1. Resend PDF's page - enter email in field.
  2. On submit, send email address to server.
  3. Fetch all customers with the given email address from internal DB. Get all associated products, and get their file paths.
  4. Send email with the files attached.

Comments requested!

ayc92 commented 5 years ago

This looks like it's written by a professional software engineer :D

Here are the pros I see with approach B

Here are the pros I see with approach A

I personally prefer approach A, because of not having to store users' emails. Whatever security we put around probably won't be as good as Stripe's.

What do you think?

kamiyo commented 5 years ago

Yea that's my leaning, too. I just wish Stripe's API were a bit unified. You can use customers with orders and products and skus, but they're moving away from orders, which isn't compatible with their new checkout and payment intents. The only other way I can think of is to use their invoice API just to keep track of the items and the customer, but to not use their invoice API for actual collection. Then in Session store the Invoice ID in the client_reference_id field. When the success webhook is called, you add the purchased items to the that customer's invoice. Then when we search for the customer's purchased PDFs, we essentially use the invoice as a ledger for the customer. What do you think about that?

ayc92 commented 5 years ago

Do invoices have an expiration date?

kamiyo commented 5 years ago

I think if you don't use their automatic workflow and don't manually delete it it'll be there.

ayc92 commented 5 years ago

Seems kinda hacky? I'm okay with starting with approach A, and then seeing how we can improve it if we want.

On Fri, Sep 13, 2019 at 7:41 AM Sean Chen notifications@github.com wrote:

I think if you don't use their automatic workflow and don't manually delete it it'll be there.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/jasboys/sycpiano/issues/235?email_source=notifications&email_token=AAI4ZVDCXJODAER5F54O5Z3QJORATA5CNFSM4IWLYRBKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD6VHD3Y#issuecomment-531263983, or mute the thread https://github.com/notifications/unsubscribe-auth/AAI4ZVHBKXG34S2FJOXP47LQJORATANCNFSM4IWLYRBA .

-- Andrew Chen

kamiyo commented 5 years ago

Haha yea a bit. I mean storing the skus in the metadata portion also seems kind of hacky, but I guess less so. i wish you could store as an array of strings, but it only allows associative array, so we have to do sku0, sku1, ...

Let's go with A first then. 👍