Assessing Yuvraj's #678 testing scope against user flows and briefing Paras for #679 integration work
Yuvraj has completed 17 test cases + 5 screen recordings covering the Location Entity feature (#678: Kiosk Management with Location Assignment).
Test Report Timestamp: 24 Apr 2026, 09:05 BST
Create location on-the-fly during kiosk creation. Covers form flow and data persistence.
Verify same location can be assigned to multiple kiosks. Critical for GASDS aggregation.
Edit creates new location doc; old kiosks point to original. Essential for HMRC audit trail.
Create button blocked until name and location selected. Form validation working.
Latitude 0, Longitude 0 allowed. Invalid ranges (lat 91, lng 181) rejected with inline errors.
Location API failure shows inline error; kiosk save failure shows toast. Graceful degradation.
Editing kiosk with missing/invalid location mapping triggers warning and blocks save.
Non-admin accounts cannot create/edit kiosks or locations. RBAC enforced.
Post-deployment verification: no "missing index" error when loading locations list.
isCommunityBuilding Toggle
Spec requires boolean toggle for GASDS community building rules. Tests mention "fill required fields" but no explicit mention of testing this toggle. Needs confirmation from Yuvraj: Is this included in test 10 (required field validation)?
Test 10 checks "required" but no explicit test for UK postcode regex or edge cases (outward/inward code split, spaces). HMRC-critical field for GASDS eligibility.
Spec says default is "UK". No explicit test for this. Confirm in test 3 (create location) that country defaults to UK.
Before marking #678 as ready for merge, please confirm:
isCommunityBuilding toggle tested? (Test 10: required field validation)
The Location Entity (#678) is the foundation. It doesn't appear directly in donor-facing flows but is critical infrastructure for GASDS compliance and reporting.
location_id stored in the kiosk document.
| Entity | Represents | Key Fields | Governance |
|---|---|---|---|
| Location | Physical building | name, postcode, city, lat, lng, isCommunityBuilding |
First-class Firestore collection (immutable, new doc on edit) |
| Kiosk | Device at location | name, location_id (reference) |
Points to Location. Can be reused across kiosks. |
| Campaign | Fundraising initiative | name, complianceMode (Donation|Activity), kiosk assignments |
Carries compliance mode. Only Donation Mode is GASDS-eligible. |
| Donation | Transaction record | amount, location_id, location_snapshot, campaignMode, taxYear |
Inherits location_id and snapshot from kiosk at payment time. Immutable after creation. |
Where location data is captured or used across flows:
| Flow Step | Data Captured | Source | Field Type |
|---|---|---|---|
| Admin: Kiosk Creation | kiosk.location_id |
Admin selects location | Reference (FK to locations/) |
| Flow 1: Payment Webhook | donation.location_id |
Read from kiosk document | Reference (FK) |
| Flow 1: Payment Webhook | donation.location_snapshot { name, postcode, city } |
Fetch locations/{location_id} at payment time | Embedded document (denormalized) |
| Flow 1: Gift Aid Check | Kiosk location's postcode (context only) | Kiosk document | For pre-population, not validation |
| GASDS Reporting | Sum by location_id, tax year, compliance mode | Donation queries | Aggregation key |
Once Yuvraj's #678 is merged, your job (#679) is to make donations actually use the Location entity. This is the "connecting piece" between admin setup and GASDS compliance.
Each donation must store both:
location_id — Reference to the Location document (FK). Used for GASDS aggregation queries. Enables "which building?"location_snapshot — Embedded snapshot { name, postcode, city }. Immutable copy taken at payment time. Resilient to future location edits.Why both? location_id enables efficient queries; location_snapshot ensures audit trail (if location is later edited, the donation still shows what it was at payment time).
At donation creation time, your payment webhook must:
Order of operations:
kiosk.location_idlocations/{location_id} from Firestorelocation_id and location_snapshotDo NOT skip the fetch. If you try to guess or defer, you risk stale or missing data.
Once a donation is created, its location_snapshot must NEVER be updated.
Example: If an admin later edits the location (which creates a new location document), old donations keep their original snapshot. New donations at that location get the new snapshot.
Code rule: location_snapshot is write-once. No update operations on it.
location_snapshot.postcode must ALWAYS be present. This field is needed for:
Validation: When writing a donation, check that location_snapshot.postcode is not null/empty. If missing, reject donation creation.
To calculate "How much GASDS has this building used this tax year?", the system queries:
The location_id is the grouping key. Without it, GASDS reporting is impossible. Do not allow donations without a location_id.
Only Donation Mode campaigns are GASDS-eligible. Activity Mode (madrasah fees, event registrations) can NEVER be claimed under GASDS, even at the same location.
Your donation record needs both fields:
location_id — "At which building?"campaignMode — "Is this a Donation or Activity?"GASDS calculations filter by campaignMode == 'donation'. This ensures Activity Mode donations don't count toward the £8k cap.
You can prepare your test plan and architecture now, but you cannot start coding until #678 is merged.
Ready-to-code checklist:
Once those boxes are ticked, you have everything you need.
Before writing a donation, validate:
location_id must exist and reference a valid location documentlocation_snapshot.name must be non-emptylocation_snapshot.postcode must be non-empty and match UK postcode regexlocation_snapshot.city must be non-emptycampaignMode must be 'donation' or 'activity'taxYear must be set (derived from donation date)If any validation fails, reject the donation and log an error. Do not write partial records.
donations/ collection document structure (relevant fields):
Note: location_snapshot is embedded (denormalized). donorHomePostcode is separate (donor's home, not kiosk location).
Before closing out #678 and starting #679, execute these actions.
isCommunityBuilding toggle tested in test 10 (required field validation)?| Phase | Issue | Owner | Status | Blocker |
|---|---|---|---|---|
| Phase 1 | #678: Location Entity + Kiosk Management | Yuvraj | Testing (17 tests + 5 videos) | None — ready to merge? |
| Phase 1→2 | #678 Merged to main | Yuvraj + Qamar | Pending | Yuvraj confirmations above |
| Phase 2 | #679: Donation Location Handling | Paras | Ready to prepare | #678 merged |
| Phase 3 | GASDS Reporting (future) | TBD | Blocked | #679 complete + Campaign Compliance Lock |
This is a proposed product architecture decision for team review. Qamar wants input from the dev team before finalising. Date raised: 24 April 2026.
Instead of routing compliance at the kiosk level or through a separate config screen, compliance mode is set per campaign at creation time.
Each campaign has a field: campaignMode: 'donation' | 'activity'. This single field drives all downstream compliance behaviour.
| Donation Mode | Activity / Payment Mode | |
|---|---|---|
| Stripe Rate | 1.2% + 20p (charity rate) | 1.5% + 20p (standard rate) |
| Gift Aid | Eligible — magic link offered | Hard-locked OFF — never offered |
| GASDS | Eligible (if ≤£30 and contactless) | Excluded |
| Accounting Ledger | Charitable income | Trading income |
| R68 Export | Included | Excluded |
| Record Type | type: 'donation' |
type: 'activity' |
The donor-facing [Donation] / [Payment] pathway screen is conditional — it only appears when needed.
No front door shown. Donor goes straight to campaign list. All records automatically type: 'donation'.
No front door shown. Donor goes straight to campaign list. All records automatically type: 'activity'.
Two-pathway filter appears: [Donation] / [Payment]. Filters the campaign list by mode. Donor picks pathway, then sees only campaigns of that type.
The record's compliance flag comes from the campaign mode, not from the donor's button press. The front door is just a conditional UI filter — not a data-capture step. Admin controls the experience by simply assigning or unassigning campaign types to the kiosk.
mode field drives everything: Stripe rate, Gift Aid eligibility, GASDS, R68 export, accounting ledger. No ambiguity.This requires adding a mode field to the campaign document.
// campaigns/{campaignId}
{
...existingFields,
mode: 'donation' | 'activity', // NEW — set at campaign creation
// Everything downstream derives from this:
// → Stripe rate selection
// → Gift Aid magic link eligibility
// → GASDS eligibility check
// → R68 export inclusion/exclusion
// → Accounting ledger classification
}
// Kiosk front-door logic (pseudo-code):
const campaigns = await getCampaignsForKiosk(kioskId);
const modes = [...new Set(campaigns.map(c => c.mode))];
if (modes.length === 1) {
// Single mode — skip front door, show campaign list directly
showCampaignList(campaigns);
} else {
// Mixed mode — show [Donation] / [Payment] filter
showFrontDoor(modes);
}
mode be editable after creation?
displayLabel override?
Campaign-level mode is the compliance engine — it must exist. But there are multiple ways to route donors to the right campaign without them thinking about compliance. These approaches work together, not as alternatives.
Each campaign has mode: 'donation' | 'activity'. This drives Stripe rate, Gift Aid, GASDS, R68, ledger classification. This is the compliance engine — non-negotiable.
When a kiosk has both types: the conditional front door [Donation] / [Payment] appears and filters the campaign list by mode.
✅ Covers every scenario. Single source of truth.
⚠️ Mixed-mode kiosks require a front door step — one extra tap.
Optional modeFilter on the kiosk document. A kiosk marked "Donation" only shows donation-mode campaigns. A kiosk marked "Payment" only shows activity-mode campaigns. No front door needed — the device is single-purpose.
Use case: A mosque puts one tablet at the entrance (donations) and one at the madrasah desk (fees). Volunteers don't need training. Donors tap and go.
✅ Fastest donor experience. Zero confusion. Same dashboard, full compliance on both devices.
Architecture: just an optional modeFilter: 'donation' | 'activity' | null on the kiosk document. Campaign mode is still the compliance engine.
Each campaign already gets its own QR code as a default deliverable. The QR links directly to that specific campaign — the donor never sees a campaign list or a front door. Compliance is inherited automatically from the campaign's mode.
Use case: Print QR codes on collection boxes, posters, or leaflets. "Scan here for Sadaqah" next to "Scan here for Madrasah Fees." Physical separation without multiple devices. No kiosk hardware needed.
✅ Zero hardware cost. Works anywhere. Already in the build plan.
This means every printed QR code is inherently compliant — it points to a campaign that already knows whether it's donation or activity mode.
"SwiftCause gives you three ways to collect — kiosk with smart routing, dedicated devices, or printed QR codes — and every single one is HMRC-compliant out of the box."
No competitor offers this. Dona and GoodBox are donation-only. CollecTin charges for two separate hardware units. HibaBox doesn't distinguish at all. SwiftCause does it in software, at the campaign level, across any collection method.
Approach 1 (campaign mode) is P0 and must be built pre-launch. Approach 3 (QR per campaign) is already planned. Approach 2 (kiosk mode filter) is a simple addition — one optional field on the kiosk document + a filter in the campaign list query. Should we include it in the campaign compliance mode sprint, or park it as a fast-follow?
No third button. No picker lists. No phase-2 complexity on the front door.
If future requirements introduce donor IDs, member categories, or restricted fund codes, those route via back-office rules — never the front door. The front door is either invisible (single mode) or exactly two choices (mixed mode). This is a permanent constraint.
Research into how UK charity donation platforms handle the split between charitable income (donations) and trading income (payments/fees). None of them solve this problem properly. SwiftCause's campaign-level compliance mode would be a genuine differentiator.
| Platform | Approach | Donation/Payment Split? | Gift Aid Handling | GASDS | Mixed-Use Support |
|---|---|---|---|---|---|
| Dona | Donation-only terminals | ❌ No — everything is a donation | Card-linked (register once, auto-applies) | ✅ Supported | ❌ Can't collect fees |
| GoodBox | Donation-only devices | ❌ No — donation only | Via Swiftaid partnership | ✅ Supported | ❌ Can't collect fees |
| CollecTin | Two separate hardware SKUs | ⚠️ Yes — but at device level (buy 2 units) | Via Give A Little integration | ✅ Supported | ⚠️ Need 2 devices |
| Give A Little | Software platform, Gift Aid optional per campaign | ⚠️ Partial — GA toggleable per campaign | Optional per campaign | ✅ Supported | ⚠️ Closest to our approach |
| HibaBox | Mosque-specific kiosks | ❌ No visible compliance split | HMRC export from dashboard | Unclear | ⚠️ Handles both but may not distinguish |
| SwiftCause (proposed) | Campaign-level compliance mode | ✅ Yes — full software routing | Auto-routed by campaign mode | ✅ Mode-gated | ✅ One device, full separation |
Used by: Dona, GoodBox, CollecTin (charity SKU)
Sidestep the problem. Only accept donations. If a charity needs to collect fees, tell them to use a separate system.
✅ Clean and compliant
❌ Loses mixed-use charities (mosques, churches with nurseries, event charities)
Used by: CollecTin (separate SKUs)
Sell separate hardware for each purpose. Compliance guaranteed because the device itself is single-purpose.
✅ Compliance guaranteed
❌ Terrible UX — two devices, two dashboards, double the cost
Used by: HibaBox (arguably)
Accept all payments and label them donations. The platform doesn't distinguish. This works until HMRC audits.
✅ Simple to build
❌ HMRC risk — trading income classified as charitable income with Gift Aid claimed on it
Used by: No one yet — this is novel
Campaign mode drives Stripe rate, Gift Aid, GASDS, R68, and ledger classification. One device, one dashboard, full separation.
✅ Full compliance + one device + mixed-use support
✅ Enables the market nobody else serves
Founded 2019 by Regium Consulting (London). Self-service terminals with contactless, Apple/Google Pay, Chip & PIN. Processing fee: 1.58% via Stripe.
Gift Aid: Card-linked — donor registers once, Gift Aid auto-applies to all future donations from that card. Clever UX but only works for donations.
The gap: If a mosque wants to collect Madrasah fees on the same device, Dona can't do it. The charity needs a completely separate payment system for trading income. Two systems, two reconciliation workflows.
Source: donadonations.com
FCA regulated, PCI & GDPR compliant. Offers GoodPlate (church collection plate form factor) and GBx Flex. Partners with Swiftaid for automated Gift Aid claims.
GASDS: Explicitly supported — their reporting provides when/where evidence for HMRC claims. All contactless donations under £30 qualify.
The gap: Same as Dona — donation-only. No concept of trading income. A church running a café or nursery can't collect those payments through GoodBox without creating a compliance risk.
Source: goodbox.com
UK-designed and assembled. Interesting approach: they sell two separate product SKUs:
The insight: CollecTin recognised the problem exists but solved it at the hardware level. If you need both types, you buy two devices. Compliant but expensive and operationally clunky.
Fees: 1.49% for contactless (via SumUp), 2.5% for online/QR.
Source: collectin.com
World's only dedicated "point of donation" software platform. ISO 27001 + 9001 certified. Integrates with SumUp and Stripe hardware.
Key similarity: Gift Aid is "optional on campaigns" — this is conceptually similar to setting compliance mode at the campaign level.
Where we go further: Give A Little's campaign-level Gift Aid toggle is just an on/off switch. SwiftCause's campaign mode drives six downstream systems: Stripe rate, Gift Aid eligibility, GASDS eligibility, R68 export inclusion, ledger classification, and conditional front door. That's a much deeper compliance integration.
Source: givealittle.co
Cloud-based donation management built for mosques and Islamic centres. Handles donations (zakat, sadaqah) AND program administration (Quran classes, fee collection).
The problem: They handle both donation and trading income but don't appear to distinguish between them at the compliance level. Gift Aid summaries are exportable from the dashboard, but there's no visible mechanism to prevent Gift Aid claims on trading income (madrasah fees).
The risk: If a mosque claims Gift Aid on madrasah fees collected through HibaBox, that's an HMRC compliance violation. The platform doesn't protect them from this.
Source: hibabox.com
The core HMRC principle: If the charity does not provide goods or services in return for payment, the payment has the character of a charitable donation. If goods or services are provided, it's trading income.
Gift Aid and GASDS can ONLY be claimed on charitable donations — never on trading income. Claiming Gift Aid on trading income is a compliance violation that can result in penalties and clawback.
The 2025 Code of Fundraising Practice (effective 1 November 2025) adds new requirements for contactless and "convenience giving" — charities must ensure donors can identify the charity, understand how donations are used, and see any processing fees. Unstaffed collection devices have additional transparency rules.
Records must be kept for at least 6 years from the end of the tax year they relate to.
Most charities are donation-only. Don't make them actively choose "donation" — default to it. Only surface the activity option when the admin creates a campaign involving goods/services. Consider a helper prompt: "Will donors receive goods or services in exchange for this payment?" — Yes → activity mode, No → donation mode.
The "immutable after first transaction" rule is correct. But what if an admin sets the wrong mode and processes 50 transactions before realising? For pilot: make the mode selection very prominent with a clear warning — "This cannot be changed after the first transaction is processed." Post-launch: consider a dispute/correction workflow.
"Donation" / "Payment" is generic. For a mosque: "Sadaqah" / "Madrasah Fees" is far clearer. For a church: "Offering" / "Hall Hire". A displayLabel field on campaign mode would make the UX feel purpose-built for the religious sector — your pilot market. Quick win, big impact.
The market has a gap. Competitors either dodge the donation-vs-payment problem (Dona, GoodBox), solve it with hardware (CollecTin), or ignore it (HibaBox). Nobody does software-level compliance routing at the campaign level.
SwiftCause's campaign-level compliance mode directly enables the mixed-use charity market — mosques, churches with trading arms, charities running events — that none of the competitors serve well. This is a defensible differentiator, not a catch-up feature.