From cfbbe2095d8785c2aef5136ba85e67100fb33d96 Mon Sep 17 00:00:00 2001 From: Tanner Collin Date: Thu, 25 May 2023 17:53:41 -0600 Subject: [PATCH] Allow paying for Donations and Consumables with Protocoin --- apiserver/apiserver/api/views.py | 67 ++++++++++++++++++++++++++++++++ webclient/src/Paymaster.js | 60 +++++++++++++++++++++++++++- 2 files changed, 125 insertions(+), 2 deletions(-) diff --git a/apiserver/apiserver/api/views.py b/apiserver/apiserver/api/views.py index 5a65334..c789afd 100644 --- a/apiserver/apiserver/api/views.py +++ b/apiserver/apiserver/api/views.py @@ -1150,6 +1150,71 @@ class InterestViewSet(Base, Retrieve, Create): class ProtocoinViewSet(Base): + @action(detail=False, methods=['post'], permission_classes=[AllowMetadata | IsAuthenticated]) + def spend_request(self, request): + try: + with transaction.atomic(): + source_user = self.request.user + source_member = source_user.member + + try: + balance = float(request.data['balance']) + except KeyError: + raise exceptions.ValidationError(dict(balance='This field is required.')) + except ValueError: + raise exceptions.ValidationError(dict(balance='Invalid number.')) + + try: + amount = float(request.data['amount']) + except KeyError: + raise exceptions.ValidationError(dict(amount='This field is required.')) + except ValueError: + raise exceptions.ValidationError(dict(amount='Invalid number.')) + + try: + category = str(request.data['category']) + except KeyError: + raise exceptions.ValidationError(dict(category='This field is required.')) + if category not in ['Consumables', 'Donation']: + raise exceptions.ValidationError(dict(category='Invalid category.')) + + memo = str(request.data.get('memo', '')) + + # also prevents negative spending + if amount < 0.25: + raise exceptions.ValidationError(dict(amount='Amount too small.')) + + source_user_balance = source_user.transactions.aggregate(Sum('protocoin'))['protocoin__sum'] or 0 + source_user_balance = float(source_user_balance) + + if abs(source_user_balance - balance) > 0.01: # stupid https://docs.djangoproject.com/en/4.2/ref/databases/#decimal-handling + raise exceptions.ValidationError(dict(balance='Incorrect current balance.')) + + if source_user_balance < amount: + raise exceptions.ValidationError(dict(amount='Insufficient funds.')) + + tx_memo = 'Protocoin - Transaction spent ₱ {} on {}{}'.format( + amount, + category, + ', memo: ' + memo if memo else '' + ) + + tx = models.Transaction.objects.create( + user=source_user, + protocoin=-amount, + amount=0, + number_of_membership_months=0, + account_type='Protocoin', + category=category, + info_source='System', + memo=tx_memo, + ) + utils.log_transaction(tx) + + return Response(200) + except OperationalError: + self.spend_request(request) + @action(detail=False, methods=['post'], permission_classes=[AllowMetadata | IsAuthenticated]) def send_to_member(self, request): try: @@ -1178,6 +1243,7 @@ class ProtocoinViewSet(Base): except ValueError: raise exceptions.ValidationError(dict(amount='Invalid number.')) + # also prevents negative spending if amount < 1.00: raise exceptions.ValidationError(dict(amount='Amount too small.')) @@ -1318,6 +1384,7 @@ class ProtocoinViewSet(Base): except ValueError: raise exceptions.ValidationError(dict(amount='Invalid number.')) + # also prevents negative spending if amount < 0.25: raise exceptions.ValidationError(dict(amount='Amount too small.')) diff --git a/webclient/src/Paymaster.js b/webclient/src/Paymaster.js index 63cd02e..f88a0d4 100644 --- a/webclient/src/Paymaster.js +++ b/webclient/src/Paymaster.js @@ -6,6 +6,44 @@ import { PayPalPayNow, PayPalSubscribe } from './PayPal.js'; import { MembersDropdown } from './Members.js'; import { requester } from './utils.js'; +export function PayWithProtocoin(props) { + const { token, user, refreshUser, amount, setAmount, custom } = props; + const member = user.member; + const [error, setError] = useState({}); + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); + + const handleSubmit = (e) => { + if (loading) return; + setSuccess(false); + setLoading(true); + + const data = { amount: amount, ...custom, balance: member.protocoin }; + requester('/protocoin/spend_request/', 'POST', token, data) + .then(res => { + setLoading(false); + setSuccess(true); + setAmount(''); + setError({}); + refreshUser(); + }) + .catch(err => { + setLoading(false); + console.log(err); + setError(err.data); + }); + }; + + return ( +
+ + Pay with Protocoin + + {success &&
Success!
} +
+ ); +}; + export function SendProtocoin(props) { const { token, user, refreshUser } = props; const member = user.member; @@ -76,10 +114,10 @@ export function Paymaster(props) { const { token, user, refreshUser } = props; const [pop, setPop] = useState('20.00'); const [locker, setLocker] = useState('5.00'); - const [consumables, setConsumables] = useState('20.00'); + const [consumables, setConsumables] = useState(''); const [buyProtocoin, setBuyProtocoin] = useState('10.00'); const [consumablesMemo, setConsumablesMemo] = useState(''); - const [donate, setDonate] = useState('20.00'); + const [donate, setDonate] = useState(''); const [memo, setMemo] = useState(''); const monthly_fees = user.member.monthly_fees || 55; @@ -188,6 +226,15 @@ export function Paymaster(props) { name='Protospace Consumables' custom={JSON.stringify({ category: 'Consumables', member: user.member.id, memo: consumablesMemo })} /> + +

+ + @@ -221,6 +268,15 @@ export function Paymaster(props) { name='Protospace Donation' custom={JSON.stringify({ category: 'Donation', member: user.member.id, memo: memo })} /> + +

+ +