Bug: giftCardBulkCreate removes shared tags from previously created gift cards
What are you trying to achieve?
Create gift cards in successive batches using a shared campaign tag, while preserving that tag on previously issued cards.
In Saleor 3.21.16, giftCardBulkCreate replaces the existing card associations for each supplied tag with the newly created batch. Older gift cards silently lose the shared tag.
Steps to reproduce the problem
Use a disposable Saleor 3.21.16 environment and an app/user with MANAGE_GIFT_CARD. The example uses synthetic tags and requires no customer accounts or orders.
- Run this mutation and save the returned gift-card ID as
firstCardId:
mutation CreateBatch {
giftCardBulkCreate(
input: {
count: 1
balance: { amount: 1, currency: "USD" }
isActive: false
tags: ["shared-campaign-repro"]
}
) {
giftCards {
id
tags { name }
}
errors { field code message }
}
}- Confirm the first card has
shared-campaign-repro. - Run the same mutation again, creating a second card with the same tag.
- Query the first card again:
query CheckFirstCard($firstCardId: ID!) {
giftCard(id: $firstCardId) {
id
tags { name }
events {
type
date
tags
oldTags
}
}
}The first card has lost the shared tag. Only the second card retains it. With larger batches, the tag remains associated only with the most recently created batch.
What did you expect to happen?
Both cards should retain shared-campaign-repro. Creating new gift cards with an existing tag should not remove that tag from unrelated, previously created cards.
The removal from older cards also has no corresponding TAGS_UPDATED event in the histories we inspected. Existing event snapshots can therefore show the tag while the current giftCard.tags response omits it.
Logs
The version-matched implementation is in GiftCardBulkCreate.assign_gift_card_tags, lines 162–173:
for tag_instance in tags_instances.iterator():
tag_instance.gift_cards.set(instances)This invokes set() on the reverse many-to-many relation. It replaces the tag's entire set of associated cards instead of adding the newly created cards. The method does not emit tag-removal events for the older cards affected by this replacement.
Validation performed:
- Compared existing issuance records, current tag associations, and full event histories in an affected 3.21.16 installation. The shared campaign tag remained on exactly the final creation batch.
- Reproduced the association loss locally by executing the actual
assign_gift_card_tagsmethod extracted from the 3.21.16 source against minimal equivalent Django many-to-many models with SQLite. After 16 batches of 25 synthetic cards, only the final 25 retained the shared tag. - This local reproduction isolates the assignment method; it is not a full Saleor GraphQL deployment test. The GraphQL steps above are the minimal API reproduction for maintainers.
A likely fix is to append the new associations instead of replacing existing ones, with a regression test that creates two batches sharing a tag and then re-queries the first batch. The same test should cover a tag shared by more than one older card.
An application-side workaround is to omit tags from giftCardBulkCreate and attach them afterward using individual giftCardUpdate(addTags) calls. The single-card assignment path uses instance.tags.add(...) and preserves older associations. Reusing a tag in a later bulk-create request can still remove those repaired associations until the bulk path is corrected.
Environment
- Affected Saleor version: 3.21.16, confirmed through
shop { version }. - Local isolated reproduction: Python 3.9, Django 4.2, SQLite, macOS.
- Production OS/database internals were not inspected for this report.
- Later Saleor releases have not been verified.
Source: saleor/saleor