Monday 29 July 2019

django 63 order hsitory

nick's shopping cart

postgres shopping cart table, has both nick and andy's items

nick checks out

shopping cart table, nicks items are cleared

charges are reflected on  stripe website

he click to view his orders

all his orders are collapsed and sorted by date

he can expand to view order detail

keep expanding sub collapsed categories

collapse the category that has been viewed and open the other

postgres order history table, that contains stripe charge id

#django/models

class ShoppingItem(models.Model):
    shopper = models.ForeignKey(User, on_delete=models.CASCADE, default=1)
    album = models.ForeignKey(Album, on_delete=models.CASCADE, blank=True, default=1)
    quantity = models.PositiveIntegerField(blank=True, default=0)

    def __str__(self):
        return 'shopper ' + str(self.shopper) + ' - album ' + str(self.album) + ' - quantity '+str(self.quantity)

class OrderHistory(models.Model):
    order = models.CharField(max_length=50)
    shopper = models.ForeignKey(User, on_delete=models.CASCADE)
    total = models.DecimalField(decimal_places=2,max_digits=7, default=0)
    date = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return str(self.order)+ ' ' + str(self.shopper) + ' bought '  + str(self.total) + ' on ' + str(self.date)

---------------------------------------
#django/api/serializers

class ShoppingItemsSerializer(serializers.ModelSerializer):
    class Meta:
        model = ShoppingItem
        fields = ('id', 'shopper', 'album', 'quantity')

class OrderHistorySerializer(serializers.ModelSerializer):
    class Meta:
        model = OrderHistory
        fields = ('id', 'order', 'shopper', 'total', 'date')

-----------------------------------
#django/api/url

path('cart_checkout/', apiview.CartCheckout.as_view(), name='CartCheckout'),
path('order_history/', apiview.OrderHistoryList.as_view(), name='OrderHistory'),
path('shoppingItem/<int:pk>/', apiview.ShoppingItemDetail.as_view(), name='ShoppingItemDetail'),
path('orderDetail/<int:pk>/', apiview.OrderHistoryDetail.as_view(), name='OrderDetail'),
path('shoppingItems/', apiview.ShoppingItemsList.as_view(), name='ShoppingItems'),

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

class ShoppingItemsList(APIView):
    def get(self, request, format=None):
        #for privacy, only return shoppers items
        shoppingItems = ShoppingItem.objects.filter(shopper=request.user)
        serializer = ShoppingItemsSerializer(shoppingItems, many=True)
        return Response(serializer.data)

    def post(self, request, format=None):
        #customer has item in cart
        try:
            revisitedItem = ShoppingItem.objects.get(shopper=request.user,  album=request.data.get('album'))
            revisitedItem.quantity= int(request.data.get('quantity')) + revisitedItem.quantity

            serializer = ShoppingItemsSerializer(data=model_to_dict( revisitedItem ))
            if serializer.is_valid():
                # update database
                revisitedItem.save()
                return Response(serializer.data, status=status.HTTP_202_ACCEPTED)
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        #customer select new item
        except exceptions.ObjectDoesNotExist:
            serializer = ShoppingItemsSerializer(data=request.data)
            if serializer.is_valid():
                serializer.save()
                return Response(serializer.data, status=status.HTTP_201_CREATED)
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

class ShoppingItemDetail(APIView):

    def get_object(self, pk):
        try:
            return ShoppingItem.objects.get(pk=pk)
        except ShoppingItem.DoesNotExist:
            raise Http404

    def get(self, request, pk, format=None):
        shoppingItem = self.get_object(pk)
        serializer = ShoppingItemsSerializer(shoppingItem)
        return Response(serializer.data)

    def put(self, request, pk, format=None):
        shoppintItem = self.get_object(pk)

        #only owner can edit
        if shoppintItem.shopper != request.user:
            return Response({"detail": "You do not have permission to perform this action."},
                            status= status.HTTP_403_FORBIDDEN)

        serializer = ShoppingItemsSerializer(shoppintItem, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk, format=None):
        shoppingItem = self.get_object(pk)

        # only owner can delete
        if shoppingItem.shopper != request.user:
            return Response({"detail": "You do not have permission to perform this action."},
                            status=status.HTTP_403_FORBIDDEN)

        shoppingItem.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

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)

class OrderHistoryList(APIView):
    def get(self, request, format=None):
        orders = OrderHistory.objects.filter(shopper=request.user)
        serializer = OrderHistorySerializer(orders, many=True)
        return Response(serializer.data)

    def post(self, request, format=None):
        serializer = OrderHistorySerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

class OrderHistoryDetail(APIView):
    def get_object(self, pk):
        try:
            return OrderHistory.objects.get(pk=pk)
        except OrderHistory.DoesNotExist:
            raise Http404

    def get(self, request, pk, format=None):
        stripe.api_key = settings.STRIPE_SECRET_KEY
        order = self.get_object(pk)
        try:
            #order is a model boject, request is a python dictionary
            charge=stripe.Charge.retrieve(order.order)
            return Response(charge, status=status.HTTP_200_OK)
        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 { postOrderHistory } from '../redux/actions/postOrderHistory'
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) => {
        let total = 0
        if (this.state.qty_loaded) {
            let 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
            })

            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')
                //save order to history
                const formData = new FormData();
                formData.set('shopper', this.props.users.find(user => user.username === this.props.username).id);
                formData.set('order', this.props.orderid);
                formData.set('total', total);
                this.props.dispatch(postOrderHistory(this.props.token, formData))

                //wait for 5sec, check every sec to see if postOrderHistory successful
                let i = 0;
                const waitAdd = setInterval(() => {
                    if (this.props.orderPosted) {
                        message.success('order posted')
                        //empty cart
                        this.emptyCart()
                        //display receipt
                        window.open(this.props.receipt, '_blank')
                        clearInterval(waitAdd);
                    }
                    if (i == 50) {
                        message.error('connection timed out.')
                        clearInterval(waitAdd);
                    }
                    i++;
                }, 100)

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

    emptyCart = () => {
        this.props.shoppingItems.map((item, index) => {
            this.deleteItem(item.id)
        })
    }

    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,
            orderid: store.shoppingItems.orderid,
            orderPosted: store.orderHistory.added,
        };
    }
)(ShoppingCart);

---------------------------------------
//react/pages/orderHistory

import React, { Component } from 'react';
import '../App.css';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { getOrderHistory } from '../redux/actions/getOrderHistory';
import { message, Collapse } from 'antd';
import OrderCollapse from '../partials/orderCollapse';
import { deflateSync } from 'zlib';

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

        this.state = {

        };
    }

    componentDidMount() {
        //start fetching database once logged in
        if (this.props.loggedin) {
            if (!this.props.gotOrders) {
                this.props.dispatch(getOrderHistory(this.props.token));
            }

            //wait for 5sec, check every 0.1s to see if orders are fetched
            let i = 0;
            const waitOrders = setInterval(() => {
                if (this.props.gotOrders) {

                    clearInterval(waitOrders);
                }
                if (i == 50) {
                    message.error('fetching order history timed out.')
                    clearInterval(waitOrders);
                }
                i++;
            }, 100)
        }
    }

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

        return (
            <div style={{ padding: '10px', marginTop: '10px' }}>
                <legend>Order History</legend>
                <hr />

                {this.props.gotOrders ?
                    this.props.orders.sort((a, b) => { return new Date(b.date) - new Date(a.date) })
                        .map((order, index) => {
                            return <OrderCollapse header={order.date.toString()} extra={order.total.toString()}
                                orderid={order.id} key={index} />
                        })
                    : null}

            </div>
        );
    }
}

export default connect(
    (store) => {
        return {
            loggedin: store.login.fetched,
            token: store.login.token,
            gotOrders: store.orderHistory.fetched,
            orders: store.orderHistory.orders,
        };
    }
)(OrderHistory);

-------------------------------------
//react/partials/orderCollapse

import React, { Component } from 'react';
import '../App.css';
import { connect } from 'react-redux';
import { getOrderDetail } from '../redux/actions/getOrderDetail';
import { message, Collapse } from 'antd';

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

        this.state = {
            open: false,
            content: null,
        };
    }

    jsonToArray = (jsonText) => {
        return JSON.stringify(jsonText).replace('{', '').replace('}', '').replace(/"/g, '').split(',')
    }

    orderDetail = async (orderid) => {
        await this.setState(prevState => { return { open: !prevState.open } })
        if (this.state.open) {
            this.props.dispatch(getOrderDetail(this.props.token, orderid));

            //wait for 5sec, check every 0.1s to see if order detail is fetched
            let i = 0;
            const waitOrderDetail = setInterval(() => {
                if (this.props.gotOrderDetail) {
                    const sales = this.jsonToArray(this.props.orderDetail.metadata)
                    const address = this.jsonToArray(this.props.orderDetail.billing_details.address)
                    const cardFull = this.props.orderDetail.payment_method_details.card
                    delete cardFull.checks
                    const card = this.jsonToArray(cardFull)
                    const content = <div>
                        {sales.map((sale, index) => { return <div key={'s' + index.toString()}>{sale.replace(':', ': ')}</div> })}
                        <Collapse bordered={false} style={{ backgroundColor: 'rgba(0, 0, 0, 0)' }}>
                            <Collapse.Panel header={'billing address'} >
                                {address.map((addr, index) => { return <div key={'a' + index.toString()}>{addr.replace(':', ': ')}</div> })}
                            </Collapse.Panel>
                        </Collapse>
                        <Collapse bordered={false} style={{ backgroundColor: 'rgba(0, 0, 0, 0)' }}>
                            <Collapse.Panel header={'payment card'} >
                                {card.map((c, index) => { return <div key={'c' + index.toString()}>{c.replace(':', ': ')}</div> })}
                            </Collapse.Panel>
                        </Collapse>
                    </div>
                    this.setState({ content: content })
                    clearInterval(waitOrderDetail);
                }
                if (i == 50) {
                    message.error('fetching order detail timed out.')
                    clearInterval(waitOrderDetail);
                }
                i++;
            }, 100)
        }
    }

    render() {
        const { Panel } = Collapse;

        return (
            <Collapse bordered={false} onChange={() => this.orderDetail(this.props.orderid)}
                style={{ backgroundColor: 'rgba(0, 0, 0, 0.1)' }}>
                <Panel header={this.props.header} extra={'$' + this.props.extra}>
                    {this.state.content ? this.state.content : 'fetching details'}
                </Panel>
            </Collapse>
        );
    }
}

export default connect(
    (store) => {
        return {
            token: store.login.token,
            gotOrderDetail: store.orderHistory.gotDetail,
            orderDetail: store.orderHistory.detail,
        };
    }
)(OrderCollapse);

--------------------------------------
//redux/actions/getOrderDetail

import axios from 'axios';

export function getOrderDetail(token, id) {
    return {
        type: "get_orderDetail",
        payload: axios({
            method: 'get',
            url: 'http://127.0.0.1:8000/api/orderDetail/' + id.toString() + '/',
            headers: {
                Authorization: 'Token ' + token,
            },
        })
    }
}

---------------------------------
//redux/actions/getOrderHistory

import axios from 'axios';

export function getOrderHistory(token) {
    return {
        type: "fetch_orders",
        payload: axios({
            method: 'get',
            url: 'http://127.0.0.1:8000/api/order_history/',
            headers: {
                Authorization: 'Token ' + token
            },
        })
    }
}

---------------------------------
//redux/actions/postOrderHistory

import axios from 'axios';

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

----------------------------------
//redux/reducers/orderHistoryReducer

export default function reducer(
    state = {
        orders: [],
        fetching: false,
        fetched: false,
        adding: false,
        added: false,
        deleting: false,
        deleted: false,
        updating: false,
        updated: false,
        gettingDetail: false,
        gotDetail: false,
        detail: null,
        error: ''
    },
    action
) {
    switch (action.type) {
        case "fetch_orders_PENDING": {
            return { ...state, fetching: true, fetched: false, error: '' }
        }

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

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

        case "get_orderDetail_PENDING": {
            return { ...state, gettingDetail: true, gotDetail: false, error: '' }
        }

        case "get_orderDetail_FULFILLED": {
            return {
                ...state,
                gettingDetail: false,
                gotDetail: true,
                detail: action.payload.data,
                error: ''
            }
        }

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

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

        case "add_order_FULFILLED": {
            return {
                ...state,
                adding: false,
                added: true,
                orders: [...state.orders].concat(action.payload.data),
                error: ''
            }
        }

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

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

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

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

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

        case "reset": {
            return {
                ...state,
                orders: [],
                fetching: false,
                fetched: false,
                adding: false,
                added: false,
                deleting: false,
                deleted: false,
                updating: false,
                updated: false,
                gettingDetail: false,
                gotDetail: false,
                detail: null,
                error: ''
            }
        }

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

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

            return {
                ...state,
                updating: false,
                updated: true,
                orders: [...state.orders].filter(order => order.id != data.id).concat(data),
                error: ''
            }
        }

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

        default:
            break;
    }
    return state;
}

reference:
https://stripe.com/docs/api/charges/retrieve
https://www.w3schools.com/jsref/jsref_replace.asp
https://stackoverflow.com/questions/208105/how-do-i-remove-a-property-from-a-javascript-object
https://stackoverflow.com/questions/10123953/how-to-sort-an-array-by-a-date-property

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