Gift Aid¶
Gift Aid is a UK tax relief that allows charities to reclaim 25% on donations from UK taxpayers. Badger Commerce automates the collection of Gift Aid declarations, tracking of eligible donations, and submission of claims to HMRC.
Overview¶
The Gift Aid system has three main components:
- Declaration Collection — customers declare Gift Aid eligibility at checkout or via their account
- Donation Tracking — donations are recorded with tax period, donor details, and Gift Aid status
- HMRC Submission — a scheduled job submits eligible donations to HMRC and tracks results
Data Model¶
Donation¶
Represents a single donation made through an order or subscription invoice.
| Field | Description |
|---|---|
giftAidDeclaration |
Whether Gift Aid can be claimed on this donation |
giftAidDeclarationRef |
ID of the GiftAidDeclaration that covers this donation |
giftAidTitle/Forename/Surname |
Donor name (required for HMRC claim) |
giftAidHouseNumber/Postcode |
Donor address (required for HMRC claim) |
taxPeriod |
UK tax year in YYYY/YY format (e.g. 2024/25) |
submittedToHMRCDate |
Set when the donation is successfully submitted to HMRC |
submittedAmount |
What was actually sent to HMRC, in pence — captured when the claim XML is built |
refundedAmount |
Cumulative amount of this donation, in pence, refunded to the donor |
refundedDate |
When the refunded amount last changed |
giftAidAdjustmentPence |
Claimed donation value since refunded — the charity owes HMRC this back |
giftAidAdjustmentDate |
When the donation was first flagged as needing an adjustment |
externalId |
Links to the originating order ({orderId}_{lineItemId}) or invoice (invoice_{stripeInvoiceId}) |
Two derived amounts matter when reading this entity:
claimableAmount=amount - refundedAmount, floored at zero. This — notamount— is what goes to HMRC and what the claim totals are built from.giftAidAdjustmentRequired=giftAidAdjustmentPence > 0.
GiftAidDeclaration¶
A donor's declaration that they are a UK taxpayer. Matched to donations via ownerKey.
| Field | Description |
|---|---|
ownerKey |
Composite key: lowercase forename+surname+houseNumber+postcode |
owner |
User ID of the person who made the declaration |
title/forename/surname/houseNumber/postcode |
Donor details |
A declaration covers:
- All future donations from the same donor (matched by ownerKey)
- All past donations from the same donor made within 4 years of the declaration date
GiftAidSubmissionRun¶
Represents a single execution of the scheduled submission job.
| Field | Description |
|---|---|
siteId |
The site this run processed |
taxPeriodsScanned |
List of tax periods checked (e.g. [2024/25, 2023/24, 2022/23, 2021/22]) |
totalDonationsFound |
Total eligible donations found across all periods |
totalDonationsSubmitted |
Total successfully submitted to HMRC |
newlyEligibleCount |
Donations that became eligible via retrospective declarations |
submissionCount |
Number of HMRC submissions created (one per tax period with donations) |
status |
RUNNING, SUCCESS, PARTIAL, FAILED, or NO_DONATIONS |
GiftAidSubmission¶
An individual HMRC submission for a specific tax period, created during a run.
| Field | Description |
|---|---|
runId |
Links to the parent GiftAidSubmissionRun |
taxPeriod |
The tax period this submission covers |
donationIds |
IDs of donations included in this submission |
donorCount |
Number of unique donors |
totalAmountPence |
Total donation amount in pence |
status |
PENDING, SUCCESS, or FAILED |
correlationId |
HMRC correlation ID for polling/recovery |
pollEndpoint |
HMRC-provided endpoint URL for polling |
pollAttemptCount |
Number of poll attempts made |
lastPollAttempt |
Timestamp of the last poll attempt |
hmrcPollIntervalSeconds |
HMRC-suggested poll interval |
statusLog |
Timestamped log of all state changes (created, submitted, polled, failed, etc.) |
requestXml |
Raw XML sent to HMRC (test mode only) |
responseXml |
Raw XML response from HMRC (test mode only, or on error) |
Scheduled Processing¶
GiftAidSubmissionScheduler (worker module)¶
The scheduler runs on the 7th of every month at 9:00 AM UK time (0 0 9 7 * *). Each execution:
- Iterates all active sites with a configured charity reference and gateway credentials
- Creates a
GiftAidSubmissionRunto track the overall execution - Scans 4 tax periods back from the current date
- For each tax period, fetches all unsubmitted donations (
submittedToHMRCDateis null) - Validates donor postcodes — formats to uppercase with space before final 3 characters (e.g.
de723ul→DE72 3UL) - Checks eligibility for each donation:
- If
giftAidDeclaration=true— already eligible - If
giftAidDeclaration=false— checks for a matchingGiftAidDeclarationbyownerKey. If a valid declaration exists (covers the donation date), the donation is updated togiftAidDeclaration=truewithgiftAidDeclarationRefset - Runs pre-submission validation via
DonationValidator— donations with issues are skipped and flagged - Submits eligible donations to HMRC grouped by tax period (one R68 v2 claim per period)
- Updates the run with final stats and status
GiftAidPollScheduler (worker module)¶
Polls HMRC for the status of PENDING submissions. Runs every 60 seconds (configurable via hmrc.giftaid.poll-interval-ms).
- Finds all PENDING submissions with a correlation ID
- Checks backoff timing — uses HMRC-provided poll interval, or exponential backoff: 1m, 5m, 15m, 30m, then hourly
- Polls HMRC via the correlation ID
- On success: marks submission as SUCCESS, sets
submittedToHMRCDateon donations, sends DELETE to HMRC Transaction Engine (DSP protocol requirement), sends admin notification email - On still processing: updates poll count and waits for next interval
- On failure: marks submission as FAILED with error message
- After 24 poll attempts: marks as FAILED (gave up)
GiftAidRetryListener (worker module)¶
Listens on RabbitMQ (giftaid-retry queue) for retry requests from the admin UI.
- Loads the original failed submission
- Filters donations to only those not yet submitted (in case some were submitted via another path)
- Creates a new PENDING submission record
- Submits to HMRC — if accepted immediately, marks SUCCESS; if acknowledgement, stays PENDING for poll scheduler
Retrospective Declarations¶
A key feature is handling retrospective Gift Aid declarations. When a donor makes a declaration, it covers past donations up to 4 years back. The scheduler handles this automatically:
- Every run scans all unsubmitted donations (not just those with
giftAidDeclaration=true) - Donations without a declaration are checked against existing declarations by
ownerKey - If a matching declaration is found, the donation is flagged as eligible and included in the submission
- The run tracks how many donations became
newlyEligiblethis way
Admin Notification Email¶
After a successful HMRC submission, an email is sent to the configured admin address. The email templates are located at:
- HTML: commerce-core/src/main/resources/templates/email/giftaid/admin-claim-submitted.html
- Text: commerce-core/src/main/resources/templates/email/giftaid/admin-claim-submitted-text.html
These are in templates/email/ (not under a theme directory) so they are resolvable from both the web app and the worker.
Deduplication¶
- Donations are only submitted once —
submittedToHMRCDateis set on success - The query
findUnsubmittedDonationsByTaxPeriodAndSiteIdonly returns donations wheresubmittedToHMRCDateis null - Fully refunded donations are skipped by the scheduler and the retry listener, and partially refunded ones are claimed at their
claimableAmount - Stale
PENDINGsubmissions are automatically marked asFAILED(configurable viahmrc.giftaid.pending-stale-minutes, default 30) - The HMRC correlation ID is saved on the
GiftAidSubmissionrecord immediately after the API call, enabling crash recovery. The GovTalk XML protocol does not support client-side idempotency tokens — the PENDING→correlationId→SUCCESS/FAILED flow is the recovery mechanism
Refunds¶
HMRC only lets a charity reclaim tax on money the donor actually gave, so refunding an order that
carried a Gift Aid donation has to change the claim. GiftAidRefundService (interface in common,
GiftAidRefundServiceImpl in commerce-core) does that reconciliation.
How a refund is attributed to the donation¶
Refunds on this platform are amounts, not line selections — none of the three refund paths lets an operator pick which lines to refund. The rule is therefore:
The non-donation value of the order absorbs the refund first. Only what is left over reduces the donations, oldest line item first.
So refunding the goods and keeping the donation leaves the claim untouched, which is the usual case. Refunding more than the goods were worth eats into the donation by the excess. A donation is never pushed below zero, and any remainder beyond the order's donation value is ignored.
The alternative — pro rata across every line — was rejected: it would reduce a claim on money the donor never got back, under-claiming on genuine donations and manufacturing spurious HMRC adjustments on donations that were already claimed.
What happens next depends on whether HMRC already has it¶
| Donation state | Effect of the refund |
|---|---|
Not claimed (submittedToHMRCDate null, no live submission) |
refundedAmount rises, so claimableAmount falls. The next claim either omits the donation (full refund) or carries the reduced figure (partial refund). |
Claimed (submittedToHMRCDate set) or in flight (a PENDING/SUCCESS GiftAidSubmission lists it) |
Same amount bookkeeping, plus giftAidAdjustmentPence is set to submittedAmount - claimableAmount. The claim already sent to HMRC is never rewritten. |
An in-flight (PENDING) claim counts as claimed: the XML has already gone to HMRC, so the money is
as good as claimed and the adjustment is owed.
giftAidAdjustmentPence is the donation value that must come out of a later claim, not the tax
on it. Multiply by the Gift Aid rate to get the cash the charity owes HMRC.
Idempotency¶
The amount passed to reconcileRefund is the payment provider's cumulative refunded total for
the order, never the size of one refund. refundedAmount is set to that absolute figure and only
ever ratchets upward, so the same refund arriving twice — the admin pipeline and the Stripe
charge.refunded webhook both fire for an admin refund — cannot compound, and a stale webhook
carrying a lower total is ignored.
Reconciliation never throws: the money has already moved at the payment provider, so a Gift Aid bookkeeping failure is logged, not propagated.
Refund entry points¶
| Entry point | Module | Cumulative total passed |
|---|---|---|
RefundOrderChargeProcessor (the refundOrder pipeline, used by admin fulfilment and POST /admin/orders/{id}/refund) |
commerce-core | paymentDetails.refundedAmount after the refund is added |
ChargeRefundedHandler (Stripe charge.refunded webhook) |
worker | charge.amountRefunded |
PosRefundServiceImpl (till refunds) |
commerce-core | The charge's refunded total, re-read from Stripe |
Spotting adjustments¶
Flagged donations are listed at /admin/donations?filter=gift-aid-adjustment ("HMRC Adjustments"),
badged in the donations table, and shown on the donation detail page with the amount to deduct from
the next claim. Acting on it is a manual step — the platform does not send HMRC an adjustment.
Configuration¶
Application-level (worker application.yml)¶
These are the Badger ISV credentials for the HMRC GovTalk gateway:
hmrc:
giftaid:
vendor-id: ${HMRC_VENDOR_ID:7985}
product-name: ${HMRC_PRODUCT_NAME:Badger Commerce}
product-version: ${HMRC_PRODUCT_VERSION:1.0}
pending-stale-minutes: ${HMRC_PENDING_STALE_MINUTES:30}
poll-interval-ms: ${HMRC_POLL_INTERVAL_MS:60000}
Global Configuration (System > Global Config)¶
| Key | Description |
|---|---|
giftaid-hmrc-test-mode |
Enable/disable HMRC test mode (uses test gateway endpoint) |
Site-level Configuration (Site > Config > giftaid-config)¶
| Key | Type | Required | Description |
|---|---|---|---|
giftaid-charity-reference |
text | Yes | HMRC Charity Reference (e.g. XR12345). Sites without this are skipped. |
giftaid-gateway-username |
secret | Yes | Government Gateway username (SenderID) for HMRC submissions |
giftaid-gateway-password |
secret | Yes | Government Gateway password |
giftaid-org-name |
text | Yes | Charity organisation name as registered with HMRC |
giftaid-sender-type |
dropdown | Yes | HMRC sender type (default: Company). Options: Individual, Company, Agent, Bureau, Partnership, Trust, Employer, Government, Acting in Capacity, Other |
giftaid-regulator-name |
dropdown | Yes | Charity regulator (default: CCEW). Options: CCEW, OSCR, CCNI |
giftaid-regulator-number |
text | Yes | Charity registration number with the regulator |
giftaid-official-forename |
text | Yes | Forename of the authorised official for Gift Aid claims |
giftaid-official-surname |
text | Yes | Surname of the authorised official |
giftaid-official-postcode |
text | Yes | Postcode of the authorised official |
giftaid-official-phone |
text | Yes | Phone number of the authorised official |
giftaid-admin-notification-email |
text | No | Email address for submission notifications |
HMRC Integration¶
R68 v2 GovTalk XML Gateway¶
The implementation uses the HMRC GovTalk XML gateway with the R68 v2 schema (http://www.govtalk.gov.uk/taxation/charities/r68/2):
- Test endpoint: https://test-transaction-engine.tax.service.gov.uk/submission
- Live endpoint: https://transaction-engine.tax.service.gov.uk/submission
Submission flow: 1. Build R68 v2 XML with AuthOfficial, Declaration, Claim (OrgName, HMRCref, Regulator, Repayment with GAD entries, EarliestGAdate) 2. Calculate IRmark — SHA-1 hash of the C14N-canonicalised Body element (IRmark element removed before hashing) 3. Insert IRmark value into XML 4. POST to submission endpoint 5. Parse response — if acknowledgement, save correlation ID for polling; if error, mark FAILED 6. Poll scheduler picks up PENDING submissions and polls HMRC until success/failure 7. On final response, send DELETE request to HMRC Transaction Engine (DSP protocol requirement)
IRmark calculation:
- Canonicalise the <Body> element (including tags) using C14N without comments
- Remove all <IRmark> elements (wildcard namespace) before canonicalisation
- SHA-1 hash the canonical bytes, Base64-encode
- Verified against HMRC's official valid sample (CharitiesValidSamples.zip)
Future: HMRC Developer Hub REST API¶
An application has been registered on the HMRC Developer Hub (Application ID: 68b03431-6489-4fbb-8b7c-910563b17eda). Migration to the modern REST API with OAuth2 authentication is planned.
Donation Migration¶
DonationMigrationScheduler (worker module)¶
A one-time migration job that creates Donation records from historical orders:
- Runs once on startup (initialDelay = 30000, fixedDelay = Long.MAX_VALUE)
- Processes orders in valid states (SUBMITTED, PACKING, DISPATCHED, DELIVERED)
- Creates donations and Gift Aid declarations from order line item attributes
- Records last run timestamp to process only new orders on subsequent starts
GiftAidDeclarationChecker (commerce-core)¶
Shared service for checking and fixing Gift Aid declarations:
- Handles both order-based and invoice-based (subscription) donations
- Populates missing donor details from orders, invoices, or user profiles
- Checks for retrospective declarations by ownerKey
Admin UI¶
Navigate to Donations > HMRC Submissions (/admin/donations/hmrc-submissions):
- Run list — shows all scheduled runs with status, donations found/submitted, and newly eligible count
- Run detail — shows per-period HMRC submissions within a run
- Submission detail — shows individual donations, status log timeline, request/response XML (test mode), and retry button for failed submissions
Other donation views:
- /admin/donations — all donations
- /admin/donations/giftaid — donations with Gift Aid
- /admin/donations/current-tax-year — current tax year donations
- /admin/donations/pending-hmrc — Gift Aid donations not yet submitted
- /admin/donations/submitted-hmrc — donations already submitted to HMRC
- /admin/donations?filter=gift-aid-adjustment — donations refunded after their Gift Aid was claimed, which need adjusting out of a later claim
- /admin/donations/validation-issues — donations whose Gift Aid details fail HMRC validation
- /admin/donations/declarations — Gift Aid Declarations list (name, house number, postcode, account), newest first. The Account column shows the owning user's email address, linked to their admin profile; declarations whose owner no longer resolves to a user on this site show the raw id in grey. Free-text search matches forename, surname and postcode, and also resolves the term against user email/name so searching by account email works.
Key Files¶
| File | Module | Purpose |
|---|---|---|
Donation.java |
common | Donation entity |
GiftAidDeclaration.java |
common | Declaration entity |
GiftAidSubmission.java |
common | Per-period HMRC submission entity (with statusLog) |
GiftAidSubmissionRun.java |
common | Scheduled run entity |
GiftAidRetryEvent.java |
common | Retry event model for RabbitMQ |
GiftAidKeyGenerator.java |
common | Owner key generation, tax period calculation, declaration validation |
DonationValidator.java |
worker | Pre-submission validation of donor fields |
GiftAidSubmissionScheduler.java |
worker | Scheduled HMRC submission job (7th of each month, 9 AM UK) |
GiftAidPollScheduler.java |
worker | Polls HMRC for pending submission results |
GiftAidRetryListener.java |
worker | Handles admin retry requests via RabbitMQ |
DonationMigrationScheduler.java |
worker | One-time order-to-donation migration |
GiftAidDeclarationChecker.java |
commerce-core | Shared eligibility checking |
GiftAidRefundService.java |
common | Refund-to-claim reconciliation contract |
GiftAidRefundServiceImpl.java |
commerce-core | Reduces or flags donations when an order is refunded |
HmrcGiftAidClient.java |
worker | HMRC GovTalk XML client (R68 v2) |
IRMark.java |
worker | IRmark calculation (C14N + SHA-1) |
DonationAdminController.java |
web-admin | Admin UI controller |