Thursday 25 July 2019

django 62 stripe checkout

nick's cart

click pay with card, fill email, address

fill card info, pay

transaction processed, receipt page opens

nick's cart is emptied

stripe keeps track payments, click top one

payment method displayed

followed by Metadata that contains sales of items
#powershell

pip install stripe

-----------------------------
#django/urls

path('cart_checkout/', apiview.CartCheckout.as_view(), name='CartCheckout'),

-----------------------------------
sign up stripe to obtain keys

--------------------------------
#django/apiview

class CartCheckout(APIView):
    def post(self, request, format=None):
        stripe.api_key = settings.STRIPE_SECRET_KEY
        token = request.data.get('token')
        cart = request.data.get('cart')

        try:
            customer=stripe.Customer.create(email=token['email'],source=token['id'])
            charge=stripe.Charge.create(
                amount=cart['price']*100,
                currency='cad',
                receipt_email= token['email'],
                customer=customer.id,
                metadata=cart['items'],
            )
            return Response(charge, status=status.HTTP_202_ACCEPTED)
        except Exception as e:
            return Response(e, status=status.HTTP_400_BAD_REQUEST)

-------------------------------
//react/pages/shoppingCart

import React, { Component } from 'react';
import '../App.css';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { getShoppingItems } from '../redux/actions/getShoppingIItems';
import { getAlbums } from '../redux/actions/getAlbums';
import { getUsers } from '../redux/actions/getUser';
import { putShoppingItem } from '../redux/actions/putShoppingItem';
import { deleteShoppingItem } from '../redux/actions/deleteShoppingItem'
import { checkoutCart } from '../redux/actions/checkoutCart'
import { Input, message, Tag } from 'antd';
import { Button } from 'reactstrap';
import { MdExposureNeg1, MdExposurePlus1 } from "react-icons/md";
import Stripecheckout from 'react-stripe-checkout';

class ShoppingCart extends Component {
    constructor(props) {
        super(props);

        this.state = {
            qty: [],
            qty_input: [],
            qty_loaded: false,
        };
    }

    componentDidMount() {

        //start fetching database once logged in
        if (this.props.loggedin) {
            if (!this.props.gotAlbums) {
                this.props.dispatch(getAlbums(this.props.token));
            }
            if (!this.props.gotUsers) {
                this.props.dispatch(getUsers(this.props.token));
            }
            if (!this.props.gotShoppingItems) {
                this.props.dispatch(getShoppingItems(this.props.token));
            }

            //wait for 5sec, check every 0.1s to see if shoppingItems are fetched
            let i = 0;
            const waitShoppingItem = setInterval(async () => {
                if (this.props.gotShoppingItems) {
                    await this.setState({ qty: [], qty_input: [], qty_loaded: false })

                    this.props.shoppingItems.map(async (item, index) => {
                        await this.setState(prevState => {
                            return {
                                qty: prevState.qty.concat({ album_id: item.album, qty: item.quantity }),
                                qty_input: prevState.qty_input.concat({ album_id: item.album, qty: null })
                            }
                        })

                        if (this.state.qty.length === this.props.shoppingItems.length) { this.setState({ qty_loaded: true }) }
                    })

                    clearInterval(waitShoppingItem);
                }
                if (i == 50) {
                    message.error('fetching shopping items timed out.')
                    clearInterval(waitShoppingItem);
                }
                i++;
            }, 100)
        }
    }

    add = async (album_id) => {
        await this.setState(prevState => { return { qty_input: prevState.qty_input.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: null }) } });
        this.setState(prevState => { return { qty: prevState.qty.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: prevState.qty.find(item => item.album_id === album_id).qty + 1 }) } })
    }

    subtract = async (album_id) => {
        await this.setState(prevState => { return { qty_input: prevState.qty_input.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: null }) } });
        if (this.state.qty.find(item => item.album_id === album_id).qty > 1) {
            this.setState(prevState => { return { qty: prevState.qty.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: prevState.qty.find(item => item.album_id === album_id).qty - 1 }) } })
        }
    }

    inputChange = (e, album_id) => {
        if (e.target.value > 0) {
            const newQty = parseInt(e.target.value)
            this.setState(prevState => { return { qty: prevState.qty.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: newQty }) } })
            this.setState(prevState => { return { qty_input: prevState.qty_input.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: newQty }) } })
        } else {
            message.error('entered invalid value')
        }
    }

    deleteItem = (id) => {
        this.props.dispatch(deleteShoppingItem(this.props.token, id));

        //wait for 5sec, check every sec to see if delete successful
        let i = 0;
        const waitDelete = setInterval(() => {

            if (this.props.shoppingItemDeleted) {

                clearInterval(waitDelete);
            }
            if (i == 50) {
                message.error('connection timed out.')
                clearInterval(waitDelete);
            }
            i++;
        }, 100)
    }

    updateCart = () => {

        this.state.qty.map((item, index) => {
            //only update items with qty changed
            const oldItem = this.props.shoppingItems.find(i => i.album === item.album_id)
            if (item.qty !== oldItem.quantity) {
                const formData = new FormData();
                formData.set('shopper', this.props.users.find(user => user.username === this.props.username).id);
                formData.set('album', item.album_id);
                formData.set('quantity', item.qty);

                this.props.dispatch(putShoppingItem(this.props.token, oldItem.id, formData));

                //wait for 5sec, check every sec to see if updateShoppingItem successful
                let i = 0;
                const waitUpdate = setInterval(() => {
                    const newItem = this.props.shoppingItems.find(i => i.album === item.album_id)
                    if (newItem.quantity === item.qty) {
                        message.success('Updated quantity of ' + this.props.albums.filter(album => album.id === item.album_id)[0].album_title + ' to ' + item.qty)

                        clearInterval(waitUpdate);
                    }
                    if (i == 50) {
                        message.error('connection timed out.')
                        clearInterval(waitUpdate);
                    }
                    i++;
                }, 100)
            }
        })
    }

    checkout = (token) => {
        if (this.state.qty_loaded) {
            let total = 0, i = 0, items = {}
            this.props.shoppingItems.map((item, index) => {
                const album = this.props.albums.find(album => album.id === item.album)
                total = total + album.price * item.quantity
                i++
                items['item' + i.toString()] = album.album_title + ' $' + album.price + ' x ' + item.quantity
            })

            if (i === this.props.shoppingItems.length) {
                const cartName = this.props.username + "'s Cart"
                const cart = { name: cartName, price: total, items: items }
                this.props.dispatch(checkoutCart(this.props.token, { token, cart }))
            }
        }
        else {
            message.error('please wait for cart info been retrieved')
        }

        //wait for 5sec, check every sec to see if checkout successful
        let i = 0;
        const waitCheckout = setInterval(() => {

            if (this.props.checkedout) {
                message.success('payment received')
                //display receipt
                window.open(this.props.receipt, '_blank')

                clearInterval(waitCheckout);
            }
            if (i == 50) {
                message.error('connection timed out.')
                clearInterval(waitCheckout);
            }
            i++;
        }, 100)
    }

    render() {
        if (!this.props.loggedin) {
            return <Redirect to='/login' />
        }

        let total = 0
        return (
            <div style={{ padding: '10px', marginTop: '10px' }}>
                <legend>Shopping Cart</legend>
                <hr />
                <div style={{ color: 'red' }}>{this.props.errorShoppingItem} {this.props.errorAlbum}</div>
                {
                    this.state.qty_loaded ?
                        this.props.shoppingItems
                            .sort((a, b) => { return this.props.albums.find(album => album.id === a.album).album_title.toUpperCase().localeCompare(this.props.albums.find(album => album.id === b.album).album_title.toUpperCase()) })
                            .map((item, index) => {
                                const album = this.props.albums.find(album => album.id === item.album)
                                total = total + album.price * item.quantity

                                return <div key={index}>
                                    <div style={{ width: '90%', display: 'flex', justifyContent: 'space-between' }}>
                                        <div style={{ width: '90%', display: 'flex', justifyContent: 'space-between' }}>

                                            <span style={{ fontStyle: 'italic' }}>{album.album_title} <Tag color='geekblue'>${album.price}</Tag></span>
                                            <Input
                                                type='number'
                                                onChange={(e) => { this.inputChange(e, item.album) }}
                                                style={{ width: '150px' }}
                                                addonBefore={<MdExposureNeg1 onClick={() => this.subtract(item.album)} style={{ cursor: 'pointer', fontSize: '20px' }} />}
                                                addonAfter={<MdExposurePlus1 onClick={() => this.add(item.album)} style={{ cursor: 'pointer', fontSize: '20px' }} />}
                                                value={this.state.qty_input.find(i => i.album_id === item.album).qty || this.state.qty.find(i => i.album_id === item.album).qty}
                                            />

                                        </div>
                                        <b style={{ textAlign: 'right' }} onClick={() => this.deleteItem(item.id)} style={{ cursor: 'pointer' }}>X</b>
                                    </div>
                                    <hr />
                                </div>
                            })
                        : null
                }
                <div style={{ width: '90%', display: 'flex', justifyContent: 'space-between' }}>
                    <span>Total: <Tag color='geekblue'>${total}</Tag></span>
                    <div>
                        <Button color='success' size='sm' onClick={() => this.updateCart()} style={{ marginRight: '15px' }}>Update</Button>
                        <Stripecheckout
                            stripeKey='pk_test_i8PD1nMcLZvtkCghkFKU1r0d00P9v7L9wg'
                            token={this.checkout}
                            billingAddress
                            shippingAddress
                            amount={total * 100}
                            name={this.props.username + "'s Cart"}
                            currency='CAD'
                        />
                    </div>
                </div>
            </div>
        );
    }
}

export default connect(
    (store) => {
        return {
            loggedin: store.login.fetched,
            gotShoppingItems: store.shoppingItems.fetched,
            shoppingItems: store.shoppingItems.shoppingItems,
            errorShoppingItem: store.shoppingItems.error,
            shoppingItemUpdated: store.shoppingItems.updated,
            shoppingItemDeleted: store.shoppingItems.deleted,
            albums: store.albums.albums,
            gotAlbums: store.albums.fetched,
            errorAlbum: store.albums.error,
            users: store.users.users,
            gotUsers: store.users.fetched,
            errorUser: store.users.error,
            username: store.login.username,
            token: store.login.token,
            loggedin: store.login.fetched,
            checkedout: store.shoppingItems.checkedOut,
            receipt: store.shoppingItems.receipt,
        };
    }
)(ShoppingCart);

-----------------------------------
//react/redux/actions/checkoutCart

import axios from 'axios';

export function checkoutCart(token, data) {
    return {
        type: "checkout_cart",
        payload: axios({
            method: 'post',
            url: 'http://127.0.0.1:8000/api/cart_checkout/',
            headers: {
                Authorization: 'Token ' + token,
            },
            data: data,
        })
    }
}

-------------------------------------------
//react/redux/reducers/shoppingItemsReducer

export default function reducer(
    state = {
        shoppingItems: [],
        fetching: false,
        fetched: false,
        deleting: false,
        deleted: false,
        updating: false,
        updated: false,
        adding: false,
        added: false,
        checkingOut: false,
        checkedOut: false,
        receipt: '',
        error: ''
    },
    action
) {
    switch (action.type) {
        case "fetch_shoppingItems_PENDING": {
            return { ...state, fetching: true, fetched: false, error: '' }
        }

        case "fetch_shoppingItems_FULFILLED": {
            return {
                ...state,
                fetching: false,
                fetched: true,
                shoppingItems: action.payload.data,
                error: ''
            }
        }

        case "fetch_shoppingItems_REJECTED": {
            return {
                ...state,
                fetching: false,
                error: JSON.stringify(action.payload.response.data),
            }
        }

        case "delete_shoppingItem_PENDING": {
            return { ...state, deleting: true, deleted: false, error: '' }
        }

        case "delete_shoppingItem_FULFILLED": {
            const deleteId = action.payload.config.data;

            return {
                ...state,
                deleting: false,
                deleted: true,
                shoppingItems: [...state.shoppingItems].filter(item => item.id != deleteId),
                error: ''
            }
        }

        case "delete_shoppingItem_REJECTED": {
            return {
                ...state,
                deleting: false,
                error: JSON.stringify(action.payload.response.data),
            }
        }

        case "add_shoppingItem_PENDING": {
            return { ...state, adding: true, added: false, error: '' }
        }

        case "add_shoppingItem_FULFILLED": {
            const data = action.payload.data;

            //check if shopper has item in cart
            const itemInCart = [...state.shoppingItems].filter(item => item.shopper == data.shopper && item.album == data.album)

            //not in cart, create new
            if (itemInCart.length === 0) {
                return {
                    ...state,
                    adding: false,
                    added: true,
                    shoppingItems: [...state.shoppingItems].concat(data),
                    error: ''
                }
            }
            //in cart, update
            else {
                const repurchasedItem = {
                    shopper: data.shopper,
                    album: data.album,
                    quantity: data.quantity,
                }

                return {
                    ...state,
                    adding: false,
                    added: true,
                    shoppingItems: [...state.shoppingItems].filter(item => item.album != data.album || item.shopper != data.shopper).concat(repurchasedItem),
                    error: ''
                }
            }
        }

        case "add_shoppingItem_REJECTED": {
            return {
                ...state,
                adding: false,
                error: JSON.stringify(action.payload.response.data),
            }
        }

        case "update_shoppingItem_PENDING": {
            return { ...state, updating: true, updated: false, error: '' }
        }

        case "update_shoppingItem_FULFILLED": {
            const data = action.payload.data;

            return {
                ...state,
                updating: false,
                updated: true,
                shoppingItems: [...state.shoppingItems].filter(item => item.album != data.album || item.shopper != data.shopper).concat(data),
                error: ''
            }
        }

        case "update_shoppingItem_REJECTED": {
            return {
                ...state,
                updating: false,
                error: JSON.stringify(action.payload.response.data),
            }
        }

        case "reset": {
            return {
                ...state,
                shoppingItems: [],
                fetching: false,
                fetched: false,
                deleting: false,
                deleted: false,
                updating: false,
                updated: false,
                error: ''
            }
        }

        case "checkout_cart_PENDING": {
            return { ...state, checkingOut: true, checkedOut: false, error: '' }
        }

        case "checkout_cart_FULFILLED": {
            return {
                ...state,
                checkingOut: false,
                checkedOut: true,
                shoppingItems: [],
                receipt: action.payload.data.receipt_url,
                error: ''
            }
        }

        case "checkout_cart_REJECTED": {
            return {
                ...state,
                checkingOut: false,
                error: JSON.stringify(action.payload.response.data),
            }
        }

        default:
            break;
    }
    return state;
}

reference:
https://www.youtube.com/watch?v=lkA4rmo7W6k
https://ultimatedjango.com/learn-django/lessons/create-stripe-processing-code/
https://stackoverflow.com/questions/51050116/recurring-payments-using-stripe-and-django
https://chuanshuoge2.blogspot.com/2018/09/stripe-api-node-express.html
https://stripe.com/docs/api/charges

python dict
https://www.w3schools.com/python/python_dictionaries.asp

No comments:

Post a Comment