Skip to content

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:

  1. Declaration Collection — customers declare Gift Aid eligibility at checkout or via their account
  2. Donation Tracking — donations are recorded with tax period, donor details, and Gift Aid status
  3. 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 — not amount — 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:

  1. Iterates all active sites with a configured charity reference and gateway credentials
  2. Creates a GiftAidSubmissionRun to track the overall execution
  3. Scans 4 tax periods back from the current date
  4. For each tax period, fetches all unsubmitted donations (submittedToHMRCDate is null)
  5. Validates donor postcodes — formats to uppercase with space before final 3 characters (e.g. de723ul → DE72 3UL)
  6. Checks eligibility for each donation:
  7. If giftAidDeclaration=true — already eligible
  8. If giftAidDeclaration=false — checks for a matching GiftAidDeclaration by ownerKey. If a valid declaration exists (covers the donation date), the donation is updated to giftAidDeclaration=true with giftAidDeclarationRef set
  9. Runs pre-submission validation via DonationValidator — donations with issues are skipped and flagged
  10. Submits eligible donations to HMRC grouped by tax period (one R68 v2 claim per period)
  11. 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).

  1. Finds all PENDING submissions with a correlation ID
  2. Checks backoff timing — uses HMRC-provided poll interval, or exponential backoff: 1m, 5m, 15m, 30m, then hourly
  3. Polls HMRC via the correlation ID
  4. On success: marks submission as SUCCESS, sets submittedToHMRCDate on donations, sends DELETE to HMRC Transaction Engine (DSP protocol requirement), sends admin notification email
  5. On still processing: updates poll count and waits for next interval
  6. On failure: marks submission as FAILED with error message
  7. 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.

  1. Loads the original failed submission
  2. Filters donations to only those not yet submitted (in case some were submitted via another path)
  3. Creates a new PENDING submission record
  4. 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 newlyEligible this 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 — submittedToHMRCDate is set on success
  • The query findUnsubmittedDonationsByTaxPeriodAndSiteId only returns donations where submittedToHMRCDate is null
  • Fully refunded donations are skipped by the scheduler and the retry listener, and partially refunded ones are claimed at their claimableAmount
  • Stale PENDING submissions are automatically marked as FAILED (configurable via hmrc.giftaid.pending-stale-minutes, default 30)
  • The HMRC correlation ID is saved on the GiftAidSubmission record 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