Skip to content

Commit

Permalink
Merge pull request #216 from ProgramEquity/stripe-implementation
Browse files Browse the repository at this point in the history
Stripe implementation
  • Loading branch information
manishapriya94 authored Jun 21, 2022
2 parents 13c50fc + 7e4223b commit d1e506f
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 70 deletions.
6 changes: 5 additions & 1 deletion server/__tests__/integration/lob.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,9 +353,13 @@ describe('POST /api/lob/createLetter', () => {
}

const template_id = 'tmpl_1057bb6f50f81fb'

// A test payment intent that should return status of 'succeeded'
const paymentIntentId = 'pi_3L7VXGFqipIA40A31qbflVvO'

const response = await request(app)
.post(route)
.send({ description, to, from, template_id })
.send({ description, to, from, template_id, paymentIntentId })
expect(response.status).toBe(200)
expect(response.body).toEqual({
expected_delivery_date: expect.any(String)
Expand Down
60 changes: 39 additions & 21 deletions server/routes/api/checkout.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,40 @@ const db = createClient()
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY)

router.post('/create-transaction', async (req, res) => {
const { transaction, email, campaignId, donationId } = req.body || {}
if (!transaction || !email) {
return null
const { session_id /*, email, campaignId, donationId */ } = req.body || {}
if (!session_id /*|| !email*/) {
return res.status(400).send({ error: 'No session ID' })
}
const session = await stripe.checkout.sessions.retrieve(session_id)
const customer = await stripe.customers.retrieve(session.customer)

const formattedTransaction = {
stripe_transaction_id: transaction.id,
amount: transaction.amount,
stripe_client_secret: transaction.client_secret,
currency: transaction.currency,
payment_method: transaction.payment_method,
payment_method_type: transaction.payment_method_types[0],
email // to-do: get user email from the server auth, if possible
stripe_transaction_id: session_id,
amount: session.amount_total,
currency: session.currency,
payment_method: 'something not empty', // Not sure what this is for
payment_method_type: session.payment_method_types[0],
email: session.customer_details.email // to-do: get user email from the server auth, if possible
}

try {
// Expire session?
await db('transactions').insert(formattedTransaction)
res.send({
status: 'ok'
})
res.status(200).send(formattedTransaction)
} catch (error) {
console.log({ error })
res.status(400).send()
}
})

// 1. send a request to `/create-payment-intent`
// with a `donationAmount` as string or integer
// If user doesn't select any particular `donationAmount`, send `1` in the donationAmount
// 2. This API will return the client secret. Use it to complete the transaction in the UI
// 2. This API will redirect the client to a Stripe Checkout page
// 3. Once user completes payment, will redirect back to `success_url` with
// a Stripe session_id included in the URL.

router.post('/create-payment-intent', async (req, res) => {
router.post('/create-checkout-session', async (req, res) => {
try {
const acceptableCharges = [1, 2, 20, 50]
const { donationAmount } = req.body || {}
Expand All @@ -47,13 +50,28 @@ router.post('/create-payment-intent', async (req, res) => {
return res.status(400).send({ error: 'Invalid Amount' })
}

const paymentIntent = await stripe.paymentIntents.create({
amount: parsedDonationAmount * 100, // in cents
currency: 'usd'
})
res.send({
clientSecret: paymentIntent.client_secret
const origin = req.get('origin')

const session = await stripe.checkout.sessions.create({
line_items: [
{
price_data: {
currency: 'usd',
product_data: {
name: 'Donation'
},
unit_amount: parsedDonationAmount * 100
},
quantity: 1
}
],
mode: 'payment',
allow_promotion_codes: true,
success_url: origin + '/complete?session_id={CHECKOUT_SESSION_ID}',
cancel_url: origin
})

res.json({ url: session.url })
} catch (error) {
console.log({ error })
}
Expand Down
18 changes: 15 additions & 3 deletions server/routes/api/lob.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,25 @@ router.post('/createAddress', async (req, res) => {
})

router.post('/createLetter', async (req, res) => {
// Get description, to, and template_id from request body
const { description, to, from, template_id, charge } = req.body || {}
// Get description, to, and template_id, and paymentIntent id from request body
const { description, to, from, template_id, paymentIntentId } = req.body || {}
const lobApiKey = getLobApiKey()
const lob = new Lob({ apiKey: lobApiKey })
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY)

try {
// Check for completed payment before creating letter. Status can be succeeded, failed, or pending. Return server error if failure or pending.
const paymentVerification = await stripe.paymentIntents.retrieve(
paymentIntentId
)

if (paymentVerification.status !== 'succeeded') {
return res
.status(500)
.json({ msg: 'Payment is still pending or has failed.' })
.end()
}

// Create Lob address using variables passed into route via post body
const letter = await lob.letters.create({
description: description,
Expand All @@ -174,7 +186,7 @@ router.post('/createLetter', async (req, res) => {
} catch (error) {
// We'll need a stripe test env key to test this in our integration tests
const refund = await stripe.refunds.create({
charge: charge
payment_intent: paymentIntentId
})
// TODO handle error for refund error. Not doing this currently because chance of
// user making it this far in the process and both LOB API and Stripe failing is very small.
Expand Down
27 changes: 21 additions & 6 deletions src/components/ActionComplete.vue
Original file line number Diff line number Diff line change
@@ -1,28 +1,43 @@
<template lang="html">
<section class="action-complete">
<h1>action-complete Component</h1>
<h1>
Thank you {{ data.email }} for your ${{ data.amount * 0.01 }} donation!
</h1>
</section>
</template>

<script lang="js">
import axios from 'axios'
export default {
name: 'ActionComplete',
props: [],
data () {
return {
data: {
email: null,
amount: null
}
}
},
computed: {
},
mounted () {
this.amount = this.getSession()
},
methods: {
}
getSession() {
axios.defaults.baseURL = '//localhost:6000/';
const session_id = {'session_id': this.$route.query.session_id }
axios.post( '/api/checkout/create-transaction', session_id )
.then((response) => {
this.data = response.data
})
.catch(function (error) {
console.log(error)
})
},
}
}
</script>

Expand Down
41 changes: 22 additions & 19 deletions src/components/DonateMoney.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,45 +8,48 @@
representatives, from the comfort of your home or on the go.
</p>

<v-btn-toggle v-model="text" tile color="deep-purple accent-3" group>
<v-btn elevation="2" raised value="2"> 2 </v-btn>
<v-btn-toggle v-model="donation" tile color="deep-purple accent-3" group>
<v-btn elevation="2" raised :value="2"> 2 </v-btn>

<v-btn elevation="2" raised value="20"> 20 </v-btn>
<v-btn elevation="2" raised :value="20"> 20 </v-btn>

<v-btn elevation="2" raisedvalue="50"> 50 </v-btn>
<v-btn elevation="2" :value="50"> 50 </v-btn>
</v-btn-toggle>
</v-col>
<div>
<v-btn outlined color="primary" text @click="submit"> Submit</v-btn>
<v-btn outlined color="primary" text @click="submit"> Submit </v-btn>
</div>
</section>
</template>

<script lang="js">
import axios from 'axios'
export default {
name: 'donate-money',
name: 'DonateMoney',
props: [],
mounted () {
},
data () {
return {
donation:1
}
},
computed: {
},
mounted () {
},
methods: {
submit () {
this.$router.push('/complete')
axios.post('/api/checkout/create-checkout-session',
{donationAmount: this.donation})
.then((response) => {
console.log(response);
location.href = response.data.url
})
.catch(function (error) {
console.log(error)
})
}
},
computed: {
}
}
</script>

<style scoped lang="less">
.donate-money {
}
</style>
<style scoped lang="less"></style>
32 changes: 12 additions & 20 deletions src/components/SignName.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
label="Full Name"
placeholder="John Doe"
required
></v-text-field>
/>
<v-text-field
ref="line1"
v-model="line1"
Expand All @@ -26,15 +26,15 @@
placeholder="Snowy Rock Pl"
counter="25"
required
></v-text-field>
/>
<v-text-field
ref="line2"
v-model="line2"
label="Address Line"
placeholder="Snowy Rock Pl"
counter="25"
required
></v-text-field>
/>

<v-text-field
ref="city"
Expand All @@ -43,32 +43,32 @@
label="City"
placeholder="El Paso"
required
></v-text-field>
/>
<v-text-field
ref="state"
v-model="state"
:rules="[() => !!state || 'This field is required']"
label="State"
required
placeholder="TX"
></v-text-field>
/>
<v-text-field
ref="zip"
v-model="zip"
:rules="[() => !!zip || 'This field is required']"
label="ZIP / Postal Code"
required
placeholder="79938"
></v-text-field>
/>
</v-card-text>
<v-card-text> {{ message }} </v-card-text>
<v-divider class="mt-12"></v-divider>
<v-divider class="mt-12" />
<v-card-actions>
<v-btn text> Cancel </v-btn>
<v-spacer></v-spacer>
<v-spacer />
<v-slide-x-reverse-transition>
<v-tooltip v-if="formHasErrors" left>
<template v-slot:activator="{ on, attrs }">
<template #activator="{ on, attrs }">
<v-btn
icon
class="my-0"
Expand All @@ -93,7 +93,7 @@ import axios from 'axios'
export default {
name: 'sign-name',
name: 'SignName',
data: () => ({
errorMessages: '',
Expand Down Expand Up @@ -153,20 +153,12 @@ export default {
this.$refs[f].validate(true)
})
axios.post('/api/lob/addressVerification', this.form)
axios.post('/api/lob/createAddress', this.form)
.then((response) => {
console.log(response)
console.log(this.form)
this.message = 'Address verified!'
if (response.status === 200) {
return axios.post('/api/lob/createAddress', response.data)
.then((response) => {
console.log(response)
})
.catch(function (error) {
console.log(error)
})
}
})
.catch(function (error) {
console.log(error)
Expand Down

0 comments on commit d1e506f

Please sign in to comment.