v6.4.5
Fixed
π― Points β duplicated awards
- Completing an event awarded every attendance a second time.
SP_Attendance::mark()writes points to the log immediately but never set thepoints_processedflag.SP_Events::complete_event()then callsprocess_event_points(), which settles every record whose flag is still0β i.e. all of them. Marking an event complete therefore duplicated the attendance and penalty points of every member.mark()now setspoints_processed = 1,process_event_points()carries a dedupe key, andcomplete_event()returns early when the event is already completed. A migration backfills the flag on historical rows so completing an old event no longer re-awards it. sp_points_loghad no idempotency key.SP_Points::add()was a bare INSERT, so any repeated call β double-tap, retried AJAX, refreshed POST β appended a second row and permanently doubled the balance (the log is the source of truth forget_balance()and the leaderboard). Added adedupe_keycolumn with a UNIQUE index and an optional$dedupe_keyargument; repeat awards are now rejected by the database and reported back as['duplicate' => true]. NULL keys still repeat, so manual adjustments are unaffected.- Concurrent awards lost an update.
add()read the balance from thesp_points_balanceuser-meta cache, which can be stale, then wrote backbalance + points. Two overlapping requests both read the same value and one award vanished frombalance_after. The balance is now derived from the log inside a transaction withSELECT β¦ FOR UPDATE, which serialises awards per user. typewas an enum of 6 values while the code wrote 25. Values such asattendance,birthday_rewardandbus_booking_refundwere silently coerced to''. That broke the duplicate guard inrefund_bus_booking_fee(), which matches ontype = 'bus_booking_refund'and so never fired.typeis nowvarchar(40).- Two
add()calls passed their arguments in the wrong order, writing the reason string into theevent_idcolumn: excuse submission/denial (SP_Excuses) and waiting-list bus fees (SP_Bus). - Manual points adjustment re-ran on browser refresh. The admin screens render the POST result inline instead of redirecting, so refresh or Back resubmitted the adjustment. Both forms now carry a single-use token that doubles as the award's dedupe key.
- Birthday-congratulation rollback deleted the wrong row. It used
$wpdb->insert_idafteradd()had run its own INSERT, so the id no longer referred to the congratulation record.
Idempotency keys were also added to the once-per-occurrence awards that previously relied on a racy check-then-act guard: birthday, feast day, profile completion, story quiz, service instructions, birthday gift, birthday congratulations, push-notification subscription, appeal decisions, excuse submission/denial, lesson-prep approval and quiz passes, bus booking fees and refunds, quiz best-score top-ups, and point sharing (deduped over a one-minute window so a deliberate repeat gift still works).