Allow paying course fees with Protocoin

This commit is contained in:
Tanner Collin 2023-05-26 14:17:49 -06:00
parent cfbbe2095d
commit c8378374b0
4 changed files with 66 additions and 11 deletions

View File

@ -250,7 +250,7 @@ def check_training(data, training_id, amount):
if training.attendance_status == 'Waiting for payment': if training.attendance_status == 'Waiting for payment':
training.attendance_status = 'Confirmed' training.attendance_status = 'Confirmed'
training.paid_date = datetime.date.today() training.paid_date = utils.today_alberta_tz()
training.save() training.save()
logger.info('IPN - Amount valid for training cost, id: ' + str(training.id)) logger.info('IPN - Amount valid for training cost, id: ' + str(training.id))

View File

@ -1157,6 +1157,8 @@ class ProtocoinViewSet(Base):
source_user = self.request.user source_user = self.request.user
source_member = source_user.member source_member = source_user.member
training = None
try: try:
balance = float(request.data['balance']) balance = float(request.data['balance'])
except KeyError: except KeyError:
@ -1175,9 +1177,33 @@ class ProtocoinViewSet(Base):
category = str(request.data['category']) category = str(request.data['category'])
except KeyError: except KeyError:
raise exceptions.ValidationError(dict(category='This field is required.')) raise exceptions.ValidationError(dict(category='This field is required.'))
if category not in ['Consumables', 'Donation']: if category not in ['Consumables', 'Donation', 'OnAcct']:
raise exceptions.ValidationError(dict(category='Invalid category.')) raise exceptions.ValidationError(dict(category='Invalid category.'))
if category == 'OnAcct':
try:
training_id = int(request.data['training'])
except KeyError:
raise exceptions.ValidationError(dict(training='This field is required.'))
except ValueError:
raise exceptions.ValidationError(dict(training='Invalid number.'))
training = get_object_or_404(models.Training, id=training_id)
if not training.session:
raise exceptions.ValidationError(dict(training='Invalid session.'))
if training.session.is_cancelled:
raise exceptions.ValidationError(dict(training='Class is cancelled.'))
if training.paid_date:
raise exceptions.ValidationError(dict(training='Already paid.'))
if training.session.cost != amount:
msg = 'Protocoin training payment amount mismatch:\n' + str(request.data.dict())
utils.alert_tanner(msg)
raise exceptions.ValidationError(dict(training='Class cost doesn\'t match amount.'))
memo = str(request.data.get('memo', '')) memo = str(request.data.get('memo', ''))
# also prevents negative spending # also prevents negative spending
@ -1193,11 +1219,19 @@ class ProtocoinViewSet(Base):
if source_user_balance < amount: if source_user_balance < amount:
raise exceptions.ValidationError(dict(amount='Insufficient funds.')) raise exceptions.ValidationError(dict(amount='Insufficient funds.'))
tx_memo = 'Protocoin - Transaction spent ₱ {} on {}{}'.format( if training:
amount, tx_memo = 'Protocoin - Transaction spent ₱ {} on {}, session: {}, training: {}'.format(
category, amount,
', memo: ' + memo if memo else '' training.session.course.name,
) str(training.session.id),
str(training.id),
)
else:
tx_memo = 'Protocoin - Transaction spent ₱ {} on {}{}'.format(
amount,
category,
', memo: ' + memo if memo else ''
)
tx = models.Transaction.objects.create( tx = models.Transaction.objects.create(
user=source_user, user=source_user,
@ -1211,6 +1245,12 @@ class ProtocoinViewSet(Base):
) )
utils.log_transaction(tx) utils.log_transaction(tx)
if training:
if training.attendance_status == 'Waiting for payment':
training.attendance_status = 'Confirmed'
training.paid_date = utils.today_alberta_tz()
training.save()
return Response(200) return Response(200)
except OperationalError: except OperationalError:
self.spend_request(request) self.spend_request(request)

View File

@ -7,6 +7,7 @@ import { apiUrl, isAdmin, getInstructor, BasicTable, requester, useIsMobile } fr
import { NotFound } from './Misc.js'; import { NotFound } from './Misc.js';
import { InstructorClassDetail, InstructorClassAttendance } from './InstructorClasses.js'; import { InstructorClassDetail, InstructorClassAttendance } from './InstructorClasses.js';
import { PayPalPayNow } from './PayPal.js'; import { PayPalPayNow } from './PayPal.js';
import { PayWithProtocoin } from './Paymaster.js';
import { tags } from './Courses.js'; import { tags } from './Courses.js';
function ClassTable(props) { function ClassTable(props) {
@ -693,6 +694,18 @@ export function ClassDetail(props) {
name={clazz.course_data.name} name={clazz.course_data.name}
custom={JSON.stringify({ training: userTraining.id })} custom={JSON.stringify({ training: userTraining.id })}
/> />
<p/>
<PayWithProtocoin
token={token} user={user} refreshUser={refreshUser}
amount={clazz.cost}
onSuccess={() => {
refreshUser();
refreshClass();
}}
custom={{ category: 'OnAcct', training: userTraining.id }}
/>
</div> </div>
} }
</div> </div>

View File

@ -7,7 +7,7 @@ import { MembersDropdown } from './Members.js';
import { requester } from './utils.js'; import { requester } from './utils.js';
export function PayWithProtocoin(props) { export function PayWithProtocoin(props) {
const { token, user, refreshUser, amount, setAmount, custom } = props; const { token, user, refreshUser, amount, onSuccess, custom } = props;
const member = user.member; const member = user.member;
const [error, setError] = useState({}); const [error, setError] = useState({});
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -23,7 +23,9 @@ export function PayWithProtocoin(props) {
.then(res => { .then(res => {
setLoading(false); setLoading(false);
setSuccess(true); setSuccess(true);
setAmount(''); if (onSuccess) {
onSuccess();
}
setError({}); setError({});
refreshUser(); refreshUser();
}) })
@ -232,7 +234,7 @@ export function Paymaster(props) {
<PayWithProtocoin <PayWithProtocoin
token={token} user={user} refreshUser={refreshUser} token={token} user={user} refreshUser={refreshUser}
amount={consumables} amount={consumables}
setAmount={setConsumables} onSuccess={() => setConsumables('')}
custom={{ category: 'Consumables', memo: consumablesMemo }} custom={{ category: 'Consumables', memo: consumablesMemo }}
/> />
</Grid.Column> </Grid.Column>
@ -274,7 +276,7 @@ export function Paymaster(props) {
<PayWithProtocoin <PayWithProtocoin
token={token} user={user} refreshUser={refreshUser} token={token} user={user} refreshUser={refreshUser}
amount={donate} amount={donate}
setAmount={setDonate} onSuccess={() => setDonate('')}
custom={{ category: 'Donation', memo: memo }} custom={{ category: 'Donation', memo: memo }}
/> />
</Grid.Column> </Grid.Column>