Skip to content

[diary] add seeds stock support#16

Merged
capcom6 merged 2 commits into
masterfrom
codex/design-seed-stock-management-system
Mar 7, 2026
Merged

[diary] add seeds stock support#16
capcom6 merged 2 commits into
masterfrom
codex/design-seed-stock-management-system

Conversation

@capcom6

@capcom6 capcom6 commented Feb 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a new SeedStock model to track each user's seed inventory by plant type/variety and quantity
  • register seed stock in Django admin for easy management
  • add Telegram bot commands:
    • /seeds to list available seeds with stock IDs
    • /plant <seed_stock_id> to create a Plant from stock and decrement quantity
  • add migration diary/0003_seedstock.py for schema changes

Details

  • SeedStock enforces uniqueness per (user, type, variety) and auto-aligns type from variety on save
  • /plant validates command usage, ownership, and non-empty stock before creating the plant
  • welcome message now advertises seed-related commands for discoverability

Validation

  • python -m compileall backend/bot/bot.py backend/diary/models.py backend/diary/admin.py backend/diary/migrations/0003_seedstock.py
  • python backend/manage.py makemigrations diary currently fails in this environment because Django is not installed

Codex Task

Summary by CodeRabbit

  • New Features

    • /seeds — view personal seed inventory by type and variety, with usage hints.
    • /addseeds — add seeds with argument validation; updates inventory atomically and reports totals.
    • /plant — create a plant from seed stock and decrement inventory.
    • /planting — extended planting flow and handlers.
    • Per-user seed-stock resource to track quantities and enforce uniqueness.
  • UX

    • Welcome message updated to highlight seed-management commands.
  • Admin / Backoffice

    • Seed stock manageable in admin with listing, filtering, and search.
  • Chores

    • New bot make target added.
  • Bug Fixes

    • Middleware now ensures missing user/profile are set to null when absent.

@coderabbitai

coderabbitai Bot commented Feb 27, 2026

Copy link
Copy Markdown

Walkthrough

Adds a per-user SeedStock model with migration and admin, updates middleware to set missing user/profile to None, extends Telegram bot with seed-management commands (/seeds, /addseeds, /plant, planting) and welcome text, and adds a Makefile bot target.

Changes

Cohort / File(s) Summary
Data model & migration
backend/diary/models.py, backend/diary/migrations/0003_seedstock.py
Add SeedStock model (user, type, variety, quantity) with unique_together (user,type,variety), ordering, save() syncing type from variety, and migration creating table and FKs (RESTRICT for type/variety, CASCADE for user).
Admin
backend/diary/admin.py
Register SeedStockAdmin with list_display, list_filter, search_fields, and get_queryset using select_related for user, type, variety.
Bot handlers & imports
backend/bot/bot.py
Update welcome text; add handlers seeds, add_seeds, plant_from_seed_stock, and planting; import PlantType, PlantVariety, SeedStock, html (aiogram), F, and IntegrityError; implement atomic increment/decrement with concurrency handling and user validation.
Middleware
backend/bot/middlewares.py
On UserModel.DoesNotExist in UserMiddleware, set data["user"] = None and data["profile"] = None before continuing.
Tooling
Makefile
Add bot make target to run Django runbot management command and include it in .PHONY.
Container entrypoint
scripts/docker-entrypoint.sh
Remove python manage.py syncbot invocation after migrations.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor User as Telegram User
    participant Bot as Bot Handler
    participant DB as Database
    participant PlantSvc as Plant Creation Logic

    User->>Bot: /plant <seed_stock_id>
    Bot->>DB: SELECT SeedStock by id,user (select_related type,variety)
    DB-->>Bot: SeedStock or not found
    alt found & quantity > 0
        Bot->>DB: UPDATE SeedStock SET quantity = F(quantity) - 1 WHERE id=... AND quantity>0
        DB-->>Bot: rows_updated
        Bot->>PlantSvc: create Plant (user,type,variety)
        PlantSvc->>DB: INSERT Plant
        DB-->>PlantSvc: Plant saved
        Bot->>DB: SELECT SeedStock to refresh quantity
        DB-->>Bot: updated quantity
        Bot-->>User: confirmation with remaining seeds
    else not found / empty
        Bot-->>User: error message
    end
Loading
sequenceDiagram
    autonumber
    actor User as Telegram User
    participant Bot as Bot Handler
    participant DB as Database

    User->>Bot: /addseeds <type_slug> <quantity> <variety_slug>
    Bot->>DB: SELECT PlantType WHERE slug=...
    DB-->>Bot: PlantType or error
    Bot->>DB: SELECT PlantVariety WHERE slug=... AND type=...
    DB-->>Bot: PlantVariety or error
    Bot->>DB: UPDATE SeedStock SET quantity = F(quantity) + amount WHERE user=... AND type=... AND variety=...
    DB-->>Bot: rows_updated
    alt rows_updated == 0
        Bot->>DB: INSERT SeedStock (user,type,variety,quantity=amount)
        DB-->>Bot: inserted or IntegrityError
        alt IntegrityError
            Bot->>DB: retry UPDATE SeedStock SET quantity = F(quantity) + amount ...
            DB-->>Bot: rows_updated
        end
    end
    Bot-->>User: confirmation with total quantity
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[diary] add seeds stock support' accurately summarizes the main change: introducing seed stock management to the diary module.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/design-seed-stock-management-system

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/bot/bot.py`:
- Around line 186-203: The seed consumption and plant creation must be made
atomic inside plant_from_seed_stock: replace the separate async
aget()/plant.asave()/stock.asave() sequence with a single transaction.atomic()
block that queries the SeedStock using select_for_update() (e.g.,
SeedStock.objects.select_for_update().select_related("type","variety").aget(...))
to lock the row, re-check stock.quantity > 0, create and save the Plant,
decrement and save stock.quantity (stock.asave(update_fields=["quantity"])) all
within the same transaction so concurrent /plant calls cannot over-consume or
leave inconsistent state.

In `@backend/diary/models.py`:
- Around line 226-230: The Meta.unique_together ("user", "type", "variety")
won't prevent duplicates when variety is NULL; update the model Meta to replace
unique_together with two explicit UniqueConstraint entries: one constrained to
rows where variety IS NOT NULL (enforce uniqueness on user+type+variety) and one
constrained to rows where variety IS NULL (enforce uniqueness on user+type
only), using django.db.models.UniqueConstraint with
condition=models.Q(variety__isnull=False) and
condition=models.Q(variety__isnull=True) respectively; modify the Meta class in
the model where unique_together is declared (class Meta) to remove
unique_together and add the two UniqueConstraint entries so the variety-null and
variety-non-null cases are enforced.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ecc8e36 and c49e3c8.

📒 Files selected for processing (4)
  • backend/bot/bot.py
  • backend/diary/admin.py
  • backend/diary/migrations/0003_seedstock.py
  • backend/diary/models.py

Comment thread backend/bot/bot.py
Comment thread backend/diary/models.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
backend/bot/bot.py (1)

268-284: ⚠️ Potential issue | 🔴 Critical

Make /plant consume-and-create flow atomic.

Line 280–281 and Line 283–284 are separate writes without transaction/row locking. Concurrent /plant calls can over-consume stock or leave partial state.

#!/bin/bash
set -euo pipefail

# Show the critical section in plant_from_seed_stock.
cat -n backend/bot/bot.py | sed -n '268,292p'

# Confirm missing transaction/locking primitives in this handler.
rg -n 'async def plant_from_seed_stock|transaction\.atomic|select_for_update|asave\(' backend/bot/bot.py -C4

Expected verification result: the handler shows Plant.asave() and SeedStock.asave() in sequence, without transaction.atomic/select_for_update in that flow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/bot/bot.py` around lines 268 - 284, The current consume-and-create
sequence in the plant_from_seed_stock flow performs Plant.asave() and then
decreases SeedStock.quantity with separate writes, which can race; wrap the
lookup and both writes in an async database transaction and acquire a row lock
on the SeedStock using select_for_update() (i.e., use transaction.atomic()
around the block, change the initial query to use select_for_update().aget(...),
create and save the Plant and decrement/save the SeedStock inside that
transaction, and keep the existing update_fields=["quantity"] on stock.asave()
to minimize write scope; also preserve the SeedStock.DoesNotExist handling
before entering the transaction or re-check inside the lock to avoid partial
state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/bot/bot.py`:
- Line 84: The welcome message in bot.py currently uses the inconsistent
placeholder "/plant <id>"; update that string to match the canonical command
format used elsewhere ("/plant <seed_stock_id>"). Locate the welcome message
literal in bot.py (the string that begins "Welcome, {user}! I'm your plant care
assistant bot...") and replace "/plant <id>" with "/plant <seed_stock_id>" so
all user-facing help text is consistent.
- Around line 223-238: The upsert for SeedStock has a race where two concurrent
requests can both see aupdate() == 0 and call acreate(), causing an
IntegrityError; to fix, wrap the SeedStock.objects.acreate(...) call used when
updated == 0 in a try/except catching django.db.IntegrityError, and in the
except block retry the update (call SeedStock.objects.filter(user=tg_user.user,
type=plant_type, variety=variety).aupdate(quantity=F("quantity") + quantity)) to
increment the existing row, then continue to load the stock via the same
select_related(...).aget(); ensure IntegrityError is imported and handled so the
second writer recovers instead of failing.

---

Duplicate comments:
In `@backend/bot/bot.py`:
- Around line 268-284: The current consume-and-create sequence in the
plant_from_seed_stock flow performs Plant.asave() and then decreases
SeedStock.quantity with separate writes, which can race; wrap the lookup and
both writes in an async database transaction and acquire a row lock on the
SeedStock using select_for_update() (i.e., use transaction.atomic() around the
block, change the initial query to use select_for_update().aget(...), create and
save the Plant and decrement/save the SeedStock inside that transaction, and
keep the existing update_fields=["quantity"] on stock.asave() to minimize write
scope; also preserve the SeedStock.DoesNotExist handling before entering the
transaction or re-check inside the lock to avoid partial state.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c49e3c8 and eec9824.

📒 Files selected for processing (1)
  • backend/bot/bot.py

Comment thread backend/bot/bot.py Outdated
Comment thread backend/bot/bot.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
backend/bot/bot.py (1)

268-284: ⚠️ Potential issue | 🔴 Critical

Make /plant consume stock and create Plant in one transaction.

This is still non-atomic: concurrent requests can both pass Line 276 and create multiple plants from the same remaining seed, and failures between Line 281 and Line 284 can leave inconsistent state.

🔒 Suggested fix (sync transactional unit + async wrapper)
+from asgiref.sync import sync_to_async
+from django.db import transaction
@@
 `@dp.message`(Command("plant"))
 async def plant_from_seed_stock(message: Message):
@@
-    try:
-        stock = await SeedStock.objects.select_related("type", "variety").aget(
-            id=stock_id, user__telegramuser__id=message.from_user.id
-        )
-    except SeedStock.DoesNotExist:
-        await message.answer(str(_("Seed stock not found.")))
-        return
-
-    if stock.quantity <= 0:
-        await message.answer(str(_("This seed stock is empty.")))
-        return
-
-    plant = Plant(user=stock.user, type=stock.type, variety=stock.variety)
-    await plant.asave()
-
-    stock.quantity -= 1
-    await stock.asave(update_fields=["quantity"])
+    def _plant_from_stock_sync():
+        with transaction.atomic():
+            stock = (
+                SeedStock.objects.select_for_update()
+                .select_related("type", "variety")
+                .get(id=stock_id, user__telegramuser__id=message.from_user.id)
+            )
+            if stock.quantity <= 0:
+                return None, 0
+            plant = Plant.objects.create(
+                user=stock.user, type=stock.type, variety=stock.variety
+            )
+            stock.quantity -= 1
+            stock.save(update_fields=["quantity"])
+            return plant, stock.quantity
+
+    try:
+        plant, remaining = await sync_to_async(
+            _plant_from_stock_sync, thread_sensitive=True
+        )()
+    except SeedStock.DoesNotExist:
+        await message.answer(str(_("Seed stock not found.")))
+        return
+
+    if plant is None:
+        await message.answer(str(_("This seed stock is empty.")))
+        return
@@
-                plant_name=plant.name, quantity=stock.quantity
+                plant_name=plant.name, quantity=remaining
             )
         )
     )
#!/bin/bash
# Verify current implementation has no transactional lock around read/create/decrement.
cat -n backend/bot/bot.py | sed -n '255,295p'
rg -n 'select_for_update|transaction\.atomic|sync_to_async' backend/bot/bot.py -C2
In Django 5.2 async ORM, are database transactions supported directly in async code, and what pattern is recommended for transactional sections?

Reference: https://docs.djangoproject.com/en/5.2/topics/async/

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/bot/bot.py` around lines 268 - 284, The read/create/decrement must be
done atomically to prevent races: wrap the SeedStock fetch (using a SELECT ...
FOR UPDATE) and the Plant creation/stock.quantity decrement/save inside a
transactional synchronous block executed via sync_to_async; specifically, use
Django's transaction.atomic plus select_for_update on the SeedStock query (the
current SeedStock.objects...aget call) to lock the row, create
Plant(user=stock.user, type=stock.type, variety=stock.variety) and save it,
decrement stock.quantity and save with update_fields=["quantity"] all inside the
atomic block, and surface SeedStock.DoesNotExist or other DB exceptions to the
async caller while keeping the message.answer calls outside or after the
transactional block as appropriate. Ensure you reference the existing symbols:
SeedStock.objects.select_related(...).select_for_update(...),
Plant(...).asave()/or .save() inside the transaction wrapper, and use
sync_to_async to run the synchronous transaction.atomic section from the async
handler.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/diary/models.py`:
- Around line 216-221: There is a contract mismatch between the /addseeds
command (bot.py parsing of variety_slug) and the SeedStock model
(SeedStock.variety FK) — choose one fix: either make SeedStock.variety nullable
by adding null=True, blank=True on the variety ForeignKey, create and apply the
migration, and replace the current unique_together ("user","type","variety")
with a DB-appropriate constraint (e.g. a Postgres UniqueConstraint with
condition to handle NULLs or adjust logic to avoid relying on uniqueness when
variety is NULL); or make variety_slug required in the /addseeds command by
changing the bot.py parsing/validation to require 4 args, remove the code path
that sets variety=None and update the command help text/usage to no longer mark
[variety_slug] as optional; reference SeedStock.variety, PlantVariety lookup
code in bot.py, and the Meta.unique_together on SeedStock when applying the
chosen change.

---

Duplicate comments:
In `@backend/bot/bot.py`:
- Around line 268-284: The read/create/decrement must be done atomically to
prevent races: wrap the SeedStock fetch (using a SELECT ... FOR UPDATE) and the
Plant creation/stock.quantity decrement/save inside a transactional synchronous
block executed via sync_to_async; specifically, use Django's transaction.atomic
plus select_for_update on the SeedStock query (the current
SeedStock.objects...aget call) to lock the row, create Plant(user=stock.user,
type=stock.type, variety=stock.variety) and save it, decrement stock.quantity
and save with update_fields=["quantity"] all inside the atomic block, and
surface SeedStock.DoesNotExist or other DB exceptions to the async caller while
keeping the message.answer calls outside or after the transactional block as
appropriate. Ensure you reference the existing symbols:
SeedStock.objects.select_related(...).select_for_update(...),
Plant(...).asave()/or .save() inside the transaction wrapper, and use
sync_to_async to run the synchronous transaction.atomic section from the async
handler.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eec9824 and 9da25ee.

📒 Files selected for processing (3)
  • backend/bot/bot.py
  • backend/diary/migrations/0003_seedstock.py
  • backend/diary/models.py

Comment thread backend/diary/models.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
backend/bot/bot.py (1)

264-281: ⚠️ Potential issue | 🔴 Critical

Make /plant stock consume + plant creation atomic.

This is still split across separate async operations (read/check/create/decrement). Concurrent /plant calls can both pass the quantity check and over-consume stock; failures between Line 277 and Line 280 can also leave inconsistent data.

Suggested fix (single transactional critical section)
+from asgiref.sync import sync_to_async
+from django.db import transaction
@@
+@sync_to_async
+def _consume_seed_and_create_plant(stock_id: int, telegram_user_id: int):
+    with transaction.atomic():
+        stock = (
+            SeedStock.objects.select_for_update()
+            .select_related("type", "variety", "user")
+            .get(id=stock_id, user__telegramuser__id=telegram_user_id)
+        )
+        if stock.quantity <= 0:
+            return None, None
+
+        plant = Plant.objects.create(user=stock.user, type=stock.type, variety=stock.variety)
+        stock.quantity -= 1
+        stock.save(update_fields=["quantity"])
+        return plant.name, stock.quantity
@@
-    try:
-        stock = await SeedStock.objects.select_related("type", "variety").aget(
-            id=stock_id, user__telegramuser__id=message.from_user.id
-        )
-    except SeedStock.DoesNotExist:
+    try:
+        plant_name, remaining = await _consume_seed_and_create_plant(
+            stock_id, message.from_user.id
+        )
+    except SeedStock.DoesNotExist:
         await message.answer(str(_("Seed stock not found.")))
         return
-
-    if stock.quantity <= 0:
+    if plant_name is None:
         await message.answer(str(_("This seed stock is empty.")))
         return
-
-    plant = Plant(user=stock.user, type=stock.type, variety=stock.variety)
-    await plant.asave()
-
-    stock.quantity -= 1
-    await stock.asave(update_fields=["quantity"])
@@
             _("Planted: {plant_name}. Remaining seeds: {quantity}.").format(
-                plant_name=plant.name, quantity=stock.quantity
+                plant_name=plant_name, quantity=remaining
             )
         )
     )
#!/bin/bash
# Verify whether /plant is still non-atomic in current branch.
cat -n backend/bot/bot.py | sed -n '251,289p'
rg -n 'transaction\.atomic|select_for_update' backend/bot/bot.py
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/bot/bot.py` around lines 264 - 281, The plant creation and stock
decrement must be done inside a single DB transaction with a row lock to prevent
race conditions: wrap the logic in an async transaction.atomic() context and
fetch the SeedStock with select_for_update() (e.g.,
SeedStock.objects.select_related("type","variety").select_for_update().aget(id=stock_id,
user__telegramuser__id=message.from_user.id)), then inside that transaction
check stock.quantity, create and asave the Plant (Plant(user=stock.user,
type=stock.type, variety=stock.variety).asave()), decrement stock.quantity and
asave(update_fields=["quantity"]); keep the message.answer() responses for
DoesNotExist and empty-stock cases as before but ensure they occur after
acquiring the row lock or by catching DoesNotExist inside the transaction so the
read/check/create/decrement are atomic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/bot/bot.py`:
- Around line 84-85: The welcome message currently lists /addseeds with an
optional [variety_slug] which contradicts the parser; update the help text in
the welcome string (the message built and formatted with .format(user=tg_user))
to require variety_slug (change "/addseeds <type_slug> <quantity>
[variety_slug]" to "/addseeds <type_slug> <quantity> <variety_slug>") so it
matches the addseeds parsing/handler contract.

---

Duplicate comments:
In `@backend/bot/bot.py`:
- Around line 264-281: The plant creation and stock decrement must be done
inside a single DB transaction with a row lock to prevent race conditions: wrap
the logic in an async transaction.atomic() context and fetch the SeedStock with
select_for_update() (e.g.,
SeedStock.objects.select_related("type","variety").select_for_update().aget(id=stock_id,
user__telegramuser__id=message.from_user.id)), then inside that transaction
check stock.quantity, create and asave the Plant (Plant(user=stock.user,
type=stock.type, variety=stock.variety).asave()), decrement stock.quantity and
asave(update_fields=["quantity"]); keep the message.answer() responses for
DoesNotExist and empty-stock cases as before but ensure they occur after
acquiring the row lock or by catching DoesNotExist inside the transaction so the
read/check/create/decrement are atomic.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9da25ee and bc16691.

📒 Files selected for processing (1)
  • backend/bot/bot.py

Comment thread backend/bot/bot.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
backend/bot/bot.py (1)

264-280: ⚠️ Potential issue | 🔴 Critical

Make /plant stock consumption + plant creation atomic.

Line 265 reads stock, Line 277 creates the plant, and Line 280 decrements stock in separate operations. Concurrent requests can both pass Line 272 and over-consume seeds, and failures between writes can leave inconsistent state.

#!/bin/bash
# Read-only verification: inspect the /plant flow for transactional locking.
rg -n 'async def plant_from_seed_stock|stock\.quantity <= 0|plant\.asave|stock\.asave|transaction\.atomic|select_for_update' backend/bot/bot.py -C4
Suggested fix
+from asgiref.sync import sync_to_async
+from django.db import transaction
 ...
+@sync_to_async
+def _consume_seed_and_create_plant(stock_id: int, telegram_user_id: int) -> tuple[str, int]:
+    with transaction.atomic():
+        stock = (
+            SeedStock.objects.select_for_update()
+            .select_related("type", "variety", "user")
+            .get(id=stock_id, user__telegramuser__id=telegram_user_id)
+        )
+        if stock.quantity <= 0:
+            raise ValueError("empty_stock")
+
+        plant = Plant.objects.create(
+            user=stock.user,
+            type=stock.type,
+            variety=stock.variety,
+        )
+        stock.quantity -= 1
+        stock.save(update_fields=["quantity"])
+        return plant.name, stock.quantity
+
 `@dp.message`(Command("plant"))
 async def plant_from_seed_stock(message: Message):
 ...
-    try:
-        stock = await SeedStock.objects.select_related("type", "variety").aget(
-            id=stock_id, user__telegramuser__id=message.from_user.id
-        )
-    except SeedStock.DoesNotExist:
-        await message.answer(str(_("Seed stock not found.")))
-        return
-
-    if stock.quantity <= 0:
-        await message.answer(str(_("This seed stock is empty.")))
-        return
-
-    plant = Plant(user=stock.user, type=stock.type, variety=stock.variety)
-    await plant.asave()
-
-    stock.quantity -= 1
-    await stock.asave(update_fields=["quantity"])
+    try:
+        plant_name, remaining = await _consume_seed_and_create_plant(
+            stock_id=stock_id,
+            telegram_user_id=message.from_user.id,
+        )
+    except SeedStock.DoesNotExist:
+        await message.answer(str(_("Seed stock not found.")))
+        return
+    except ValueError:
+        await message.answer(str(_("This seed stock is empty.")))
+        return
 ...
             _("Planted: {plant_name}. Remaining seeds: {quantity}.").format(
-                plant_name=plant.name, quantity=stock.quantity
+                plant_name=plant_name, quantity=remaining
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/bot/bot.py` around lines 264 - 280, Wrap the seed consumption + plant
creation in a DB transaction and lock the SeedStock row with select_for_update
to prevent races: fetch the stock using
SeedStock.objects.select_for_update().select_related("type","variety").aget(...
) inside a transaction.atomic() block, re-check stock.quantity > 0 after
locking, create and asave the Plant and decrement + asave the stock within the
same transaction; keep the existing SeedStock.DoesNotExist handling but move the
locked fetch/quantity check/plant creation/stock update into the transactional
block so both operations are atomic and serializable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@backend/bot/bot.py`:
- Around line 264-280: Wrap the seed consumption + plant creation in a DB
transaction and lock the SeedStock row with select_for_update to prevent races:
fetch the stock using
SeedStock.objects.select_for_update().select_related("type","variety").aget(...
) inside a transaction.atomic() block, re-check stock.quantity > 0 after
locking, create and asave the Plant and decrement + asave the stock within the
same transaction; keep the existing SeedStock.DoesNotExist handling but move the
locked fetch/quantity check/plant creation/stock update into the transactional
block so both operations are atomic and serializable.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc16691 and 2f54ddd.

📒 Files selected for processing (1)
  • backend/bot/bot.py

@capcom6 capcom6 force-pushed the codex/design-seed-stock-management-system branch from 2f54ddd to 985326d Compare March 1, 2026 06:24

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
backend/bot/bot.py (1)

264-280: ⚠️ Potential issue | 🔴 Critical

Race condition: concurrent /plant calls can over-consume seeds.

The check-then-act pattern at lines 272-280 is not atomic. Two concurrent requests can both pass the stock.quantity > 0 check, create plants, and decrement the quantity, resulting in more plants than available seeds or negative stock values.

Wrap the operation in transaction.atomic() with select_for_update() to serialize concurrent access:

Suggested fix using atomic transaction
+from django.db import transaction
+from asgiref.sync import sync_to_async

 `@dp.message`(Command("plant"))
 async def plant_from_seed_stock(message: Message):
     # ... argument parsing ...

-    try:
-        stock = await SeedStock.objects.select_related("type", "variety").aget(
-            id=stock_id, user__telegramuser__id=message.from_user.id
-        )
-    except SeedStock.DoesNotExist:
-        await message.answer(str(_("Seed stock not found.")))
-        return
-
-    if stock.quantity <= 0:
-        await message.answer(str(_("This seed stock is empty.")))
-        return
-
-    plant = Plant(user=stock.user, type=stock.type, variety=stock.variety)
-    await plant.asave()
-
-    stock.quantity -= 1
-    await stock.asave(update_fields=["quantity"])
+    `@sync_to_async`
+    def plant_from_stock_atomic(stock_id, telegram_user_id):
+        with transaction.atomic():
+            try:
+                stock = SeedStock.objects.select_for_update().select_related(
+                    "type", "variety"
+                ).get(id=stock_id, user__telegramuser__id=telegram_user_id)
+            except SeedStock.DoesNotExist:
+                return None, "not_found"
+
+            if stock.quantity <= 0:
+                return None, "empty"
+
+            plant = Plant(user=stock.user, type=stock.type, variety=stock.variety)
+            plant.save()
+
+            stock.quantity -= 1
+            stock.save(update_fields=["quantity"])
+            return (plant, stock), None
+
+    result, error = await plant_from_stock_atomic(stock_id, message.from_user.id)
+    if error == "not_found":
+        await message.answer(str(_("Seed stock not found.")))
+        return
+    if error == "empty":
+        await message.answer(str(_("This seed stock is empty.")))
+        return
+
+    plant, stock = result
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/bot/bot.py` around lines 264 - 280, The current non-atomic
check-then-act on SeedStock (using
SeedStock.objects.select_related(...).aget(...)) allows concurrent /plant calls
to over-consume seeds; wrap the operation in a database transaction using
transaction.atomic(), fetch the stock row with select_for_update() (replace the
aget/select_related call with a select_for_update() query inside the
transaction), re-check stock.quantity > 0 inside the transaction, create and
save the Plant (Plant(user=stock.user, type=stock.type,
variety=stock.variety).asave()) and decrement/save stock.quantity
(stock.asave(update_fields=["quantity"])), and return appropriate messages if
quantity is insufficient — this serializes access and prevents race conditions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@backend/bot/bot.py`:
- Around line 264-280: The current non-atomic check-then-act on SeedStock (using
SeedStock.objects.select_related(...).aget(...)) allows concurrent /plant calls
to over-consume seeds; wrap the operation in a database transaction using
transaction.atomic(), fetch the stock row with select_for_update() (replace the
aget/select_related call with a select_for_update() query inside the
transaction), re-check stock.quantity > 0 inside the transaction, create and
save the Plant (Plant(user=stock.user, type=stock.type,
variety=stock.variety).asave()) and decrement/save stock.quantity
(stock.asave(update_fields=["quantity"])), and return appropriate messages if
quantity is insufficient — this serializes access and prevents race conditions.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f54ddd and 985326d.

📒 Files selected for processing (4)
  • backend/bot/bot.py
  • backend/diary/admin.py
  • backend/diary/migrations/0003_seedstock.py
  • backend/diary/models.py

@capcom6 capcom6 marked this pull request as ready for review March 2, 2026 02:13

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 985326d0c8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread backend/bot/bot.py Outdated
Comment thread backend/bot/bot.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1d3c205ca1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread backend/bot/bot.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
backend/bot/bot.py (1)

278-290: ⚠️ Potential issue | 🟠 Major

/plant still has a consistency gap between decrement and plant creation.

The stock decrement and Plant creation are separate writes; if plant.asave() fails, quantity is already reduced and data diverges. This is the same root issue previously raised, just with the improved decrement path.

Suggested fix
-    updated = await stock_qs.aupdate(quantity=F("quantity") - 1)
+    updated = await stock_qs.aupdate(quantity=F("quantity") - 1)

     if updated == 0:
         await message.answer(str(_("This seed stock is empty.")))
         return

-    plant = Plant(user=stock.user, type=stock.type, variety=stock.variety)
-    await plant.asave()
+    try:
+        plant = Plant(user=stock.user, type=stock.type, variety=stock.variety)
+        await plant.asave()
+    except Exception:
+        # rollback seed consumption if plant creation fails
+        await SeedStock.objects.filter(id=stock.id).aupdate(quantity=F("quantity") + 1)
+        raise
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/bot/bot.py` around lines 278 - 290, The decrement and Plant creation
must be atomic to avoid divergence: wrap the stock decrement and Plant creation
in a single database transaction (use the async transaction.atomic() context
manager) and perform the conditional decrement and creation inside it; e.g.,
acquire the row with SeedStock.objects.select_for_update().filter(id=stock.id,
quantity__gt=0) (async), perform the quantity decrement (either via F() and
aupdate or by modifying the locked instance and asave()), check the update
result (the existing updated == 0 check) and only then create and asave() the
Plant instance (Plant(...) followed by plant.asave()) inside the same
transaction so that if plant.asave() fails the decrement is rolled back.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/bot/bot.py`:
- Around line 259-260: Update the registration prompt string used in the bot
reply—replace the incorrect phrase "You must be registered in to use this
command. Use /start." (seen in the call str(_("You must be registered in to use
this command. Use /start."))) with the corrected wording "You must be registered
to use this command. Use /start." so the message reads grammatically correct;
adjust the NSLocalized/_ wrapper call accordingly where this string is
defined/emitted in bot.py.
- Around line 211-213: The TelegramUser lookup in bot.py (tg_user = await
TelegramUser.objects.select_related("user").aget(...)) can raise
TelegramUser.DoesNotExist and crash the /addseeds handler; wrap that await in a
try/except catching TelegramUser.DoesNotExist (or use an aget_or_none style
helper) inside the /addseeds command handler, and when missing respond to the
user with a short message instructing them to run /start (or otherwise register)
instead of letting the exception propagate; update any subsequent use of tg_user
in the handler to only run when the object exists.

---

Duplicate comments:
In `@backend/bot/bot.py`:
- Around line 278-290: The decrement and Plant creation must be atomic to avoid
divergence: wrap the stock decrement and Plant creation in a single database
transaction (use the async transaction.atomic() context manager) and perform the
conditional decrement and creation inside it; e.g., acquire the row with
SeedStock.objects.select_for_update().filter(id=stock.id, quantity__gt=0)
(async), perform the quantity decrement (either via F() and aupdate or by
modifying the locked instance and asave()), check the update result (the
existing updated == 0 check) and only then create and asave() the Plant instance
(Plant(...) followed by plant.asave()) inside the same transaction so that if
plant.asave() fails the decrement is rolled back.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 985326d and 1d3c205.

📒 Files selected for processing (1)
  • backend/bot/bot.py

Comment thread backend/bot/bot.py Outdated
Comment thread backend/bot/bot.py Outdated
@capcom6 capcom6 force-pushed the codex/design-seed-stock-management-system branch from 1d3c205 to bb2594c Compare March 3, 2026 07:07
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
backend/bot/bot.py (1)

292-307: ⚠️ Potential issue | 🔴 Critical

Make /plant stock decrement and Plant creation one atomic unit.

There is still a consistency gap: stock is decremented before Plant persistence is guaranteed. If creation fails after decrement, seeds are lost. Also, the write query on Line 295 drops the ownership predicate checked earlier. Please perform authorization check + decrement + plant creation in one locked transaction on the same scoped row.

🔧 Suggested direction
-    stock_qs = SeedStock.objects.filter(id=stock.id, quantity__gt=0)
-    updated = await stock_qs.aupdate(quantity=F("quantity") - 1)
-    if updated == 0:
-        await message.answer(html.quote(str(_("This seed stock is empty."))))
-        return
-
-    plant = Plant(user=stock.user, type=stock.type, variety=stock.variety)
-    await plant.asave()
+    # Move the flow into one transactional helper:
+    # 1) lock stock row scoped by id + requesting user
+    # 2) re-check quantity > 0 under lock
+    # 3) create Plant
+    # 4) decrement and save stock
+    # 5) return plant name + remaining quantity

Run this read-only check to confirm current non-atomic flow and missing lock usage:

#!/bin/bash
set -euo pipefail

echo "=== plant_from_seed_stock implementation ==="
sed -n '263,317p' backend/bot/bot.py

echo
echo "=== transaction/lock usage in bot handler file ==="
rg -n 'transaction\.atomic|select_for_update|async def plant_from_seed_stock|quantity__gt=0' backend/bot/bot.py -n -C2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/bot/bot.py` around lines 292 - 307, The current flow decrements
SeedStock via aconditional aupdate then creates Plant separately, risking lost
seeds if Plant.asave() fails and dropping the earlier ownership predicate;
change plant_from_seed_stock to run a single DB transaction that: fetches the
target SeedStock with select_for_update() (ensuring the ownership check by
including stock.user == request.user or equivalent), verify quantity > 0,
decrement quantity and save the SeedStock row, then create and save the Plant
inside the same transaction; use an async transaction.atomic context so any
failure during Plant creation rolls back the decrement and preserves
consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@backend/bot/bot.py`:
- Around line 292-307: The current flow decrements SeedStock via aconditional
aupdate then creates Plant separately, risking lost seeds if Plant.asave() fails
and dropping the earlier ownership predicate; change plant_from_seed_stock to
run a single DB transaction that: fetches the target SeedStock with
select_for_update() (ensuring the ownership check by including stock.user ==
request.user or equivalent), verify quantity > 0, decrement quantity and save
the SeedStock row, then create and save the Plant inside the same transaction;
use an async transaction.atomic context so any failure during Plant creation
rolls back the decrement and preserves consistency.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1d3c205 and bb2594c.

📒 Files selected for processing (6)
  • Makefile
  • backend/bot/bot.py
  • backend/bot/middlewares.py
  • backend/diary/admin.py
  • backend/diary/migrations/0003_seedstock.py
  • backend/diary/models.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/diary/admin.py

@capcom6 capcom6 force-pushed the codex/design-seed-stock-management-system branch from bb2594c to a22bc33 Compare March 4, 2026 04:54
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
backend/bot/bot.py (1)

292-303: ⚠️ Potential issue | 🟠 Major

Stock decrement and plant creation must be transactionally coupled to prevent data loss.

The atomic F("quantity") - 1 update prevents read-modify-write races, but if plant.asave() fails after the decrement succeeds, the user loses a seed without gaining a plant. Django 5.2 does not support async transactions directly; wrap both operations in a synchronous function and call it via sync_to_async(thread_sensitive=True):

🔧 Suggested transactional wrapper
+    from django.db import transaction
+    from asgiref.sync import sync_to_async
+
+    `@sync_to_async`
+    def decrement_and_create_plant():
+        with transaction.atomic():
+            updated = SeedStock.objects.filter(id=stock.id, quantity__gt=0).update(
+                quantity=F("quantity") - 1
+            )
+            if updated == 0:
+                return None
+            plant = Plant(user=stock.user, type=stock.type, variety=stock.variety)
+            plant.save()
+            return plant
+
-    # Atomic decrement with quantity check (prevents race conditions)
-    # Replaces non-atomic in-memory decrement to avoid concurrent request corruption
-    # Uses conditional filter + aupdate for database-level atomicity
-    stock_qs = SeedStock.objects.filter(id=stock.id, quantity__gt=0)
-    updated = await stock_qs.aupdate(quantity=F("quantity") - 1)
-
-    if updated == 0:
-        await message.answer(html.quote(str(_("This seed stock is empty."))))
-        return
-
-    plant = Plant(user=stock.user, type=stock.type, variety=stock.variety)
-    await plant.asave()
+    plant = await decrement_and_create_plant()
+    if plant is None:
+        await message.answer(html.quote(str(_("This seed stock is empty."))))
+        return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/bot/bot.py` around lines 292 - 303, The decrement and Plant creation
must be executed inside a DB transaction so the seed quantity decrement
(SeedStock.objects.filter(...).aupdate(quantity=F("quantity") - 1)) and
Plant.asave() either both succeed or both roll back; implement a synchronous
helper that opens transaction.atomic(), performs the conditional update (using
the same filter and F() expression), checks the update count, creates and saves
the Plant, and returns a result, then call that helper via
sync_to_async(thread_sensitive=True) from the async view; replace the current
separate aupdate + Plant.asave() calls with this transactional wrapper and
propagate the "out of stock" result to message.answer as before.
🧹 Nitpick comments (1)
backend/diary/models.py (1)

232-241: Defensive checks for non-nullable variety are unnecessary.

Since the variety ForeignKey is defined without null=True, it's guaranteed to be non-null at the database level. The if self.variety: checks in save() (line 233) and __str__ (line 239) are therefore always truthy and can be simplified for clarity.

♻️ Proposed simplification
     def save(self, *args, **kwargs):
-        if self.variety:
-            self.type = self.variety.type
+        self.type = self.variety.type
         super().save(*args, **kwargs)

     def __str__(self):
-        name = self.type.name
-        if self.variety:
-            name += f" {self.variety.name}"
+        name = f"{self.type.name} {self.variety.name}"
         return f"{name}: {self.quantity}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/diary/models.py` around lines 232 - 241, Remove the unnecessary
defensive checks around the non-nullable ForeignKey `variety` by directly using
`self.variety` and `self.variety.type` instead of guarding with `if
self.variety:`; update the `save` method to always assign `self.type =
self.variety.type` and simplify `__str__` to build the name using
`self.type.name` plus `self.variety.name` without conditional checks
(references: save, __str__, attributes variety and type).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@backend/bot/bot.py`:
- Around line 292-303: The decrement and Plant creation must be executed inside
a DB transaction so the seed quantity decrement
(SeedStock.objects.filter(...).aupdate(quantity=F("quantity") - 1)) and
Plant.asave() either both succeed or both roll back; implement a synchronous
helper that opens transaction.atomic(), performs the conditional update (using
the same filter and F() expression), checks the update count, creates and saves
the Plant, and returns a result, then call that helper via
sync_to_async(thread_sensitive=True) from the async view; replace the current
separate aupdate + Plant.asave() calls with this transactional wrapper and
propagate the "out of stock" result to message.answer as before.

---

Nitpick comments:
In `@backend/diary/models.py`:
- Around line 232-241: Remove the unnecessary defensive checks around the
non-nullable ForeignKey `variety` by directly using `self.variety` and
`self.variety.type` instead of guarding with `if self.variety:`; update the
`save` method to always assign `self.type = self.variety.type` and simplify
`__str__` to build the name using `self.type.name` plus `self.variety.name`
without conditional checks (references: save, __str__, attributes variety and
type).

ℹ️ Review info
Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0239089e-db41-4ce7-a466-1e650671eacb

📥 Commits

Reviewing files that changed from the base of the PR and between bb2594c and a22bc33.

📒 Files selected for processing (6)
  • Makefile
  • backend/bot/bot.py
  • backend/bot/middlewares.py
  • backend/diary/admin.py
  • backend/diary/migrations/0003_seedstock.py
  • backend/diary/models.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • Makefile

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@capcom6 capcom6 force-pushed the codex/design-seed-stock-management-system branch from ce91bce to 8a63fbc Compare March 5, 2026 01:43
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@capcom6 capcom6 added the ready label Mar 5, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
backend/bot/bot.py (1)

292-303: ⚠️ Potential issue | 🟠 Major

Decrement and plant creation are not transactional; a failure after decrement leaves inconsistent state.

The atomic F("quantity") - 1 update (line 296) prevents race conditions for the decrement, but if plant.asave() (line 303) fails afterward, the stock is already decremented with no plant created—effectively losing a seed.

Wrap both operations in a transaction to ensure consistency:

🔧 Suggested fix using transaction
+from django.db import transaction
+from asgiref.sync import sync_to_async
...

-    # Atomic decrement with quantity check (prevents race conditions)
-    # Replaces non-atomic in-memory decrement to avoid concurrent request corruption
-    # Uses conditional filter + aupdate for database-level atomicity
-    stock_qs = SeedStock.objects.filter(id=stock.id, quantity__gt=0)
-    updated = await stock_qs.aupdate(quantity=F("quantity") - 1)
-
-    if updated == 0:
-        await message.answer(html.quote(str(_("This seed stock is empty."))))
-        return
-
-    plant = Plant(user=stock.user, type=stock.type, variety=stock.variety)
-    await plant.asave()
+    `@sync_to_async`
+    def create_plant_from_stock(stock_id, user, plant_type, variety):
+        with transaction.atomic():
+            updated = SeedStock.objects.filter(id=stock_id, quantity__gt=0).update(
+                quantity=F("quantity") - 1
+            )
+            if updated == 0:
+                return None
+            plant = Plant(user=user, type=plant_type, variety=variety)
+            plant.save()
+            return plant
+
+    plant = await create_plant_from_stock(stock.id, stock.user, stock.type, stock.variety)
+    if plant is None:
+        await message.answer(html.quote(str(_("This seed stock is empty."))))
+        return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/bot/bot.py` around lines 292 - 303, Wrap the decrement and plant
creation in a single async DB transaction so the decrement is rolled back if
Plant.asave() fails: use "from django.db import transaction" and do "async with
transaction.atomic():" around the logic that filters the SeedStock
(SeedStock.objects.filter(id=stock.id, quantity__gt=0)), performs the
conditional aupdate(quantity=F('quantity') - 1) and then creates/saves the Plant
(Plant(...); await plant.asave()); keep the check on "updated == 0" inside the
same transaction and call message.answer(...) and return if no rows updated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@backend/bot/bot.py`:
- Around line 292-303: Wrap the decrement and plant creation in a single async
DB transaction so the decrement is rolled back if Plant.asave() fails: use "from
django.db import transaction" and do "async with transaction.atomic():" around
the logic that filters the SeedStock (SeedStock.objects.filter(id=stock.id,
quantity__gt=0)), performs the conditional aupdate(quantity=F('quantity') - 1)
and then creates/saves the Plant (Plant(...); await plant.asave()); keep the
check on "updated == 0" inside the same transaction and call message.answer(...)
and return if no rows updated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a839a026-c449-459c-b1cd-f3790d81f343

📥 Commits

Reviewing files that changed from the base of the PR and between ce91bce and edd068e.

📒 Files selected for processing (7)
  • Makefile
  • backend/bot/bot.py
  • backend/bot/middlewares.py
  • backend/diary/admin.py
  • backend/diary/migrations/0003_seedstock.py
  • backend/diary/models.py
  • scripts/docker-entrypoint.sh
💤 Files with no reviewable changes (1)
  • scripts/docker-entrypoint.sh
🚧 Files skipped from review as they are similar to previous changes (2)
  • Makefile
  • backend/bot/middlewares.py

@capcom6 capcom6 changed the title Add seed stock support and Telegram planting flow [diary] add seeds stock support Mar 7, 2026
@capcom6 capcom6 merged commit 0281a59 into master Mar 7, 2026
6 checks passed
@capcom6 capcom6 deleted the codex/design-seed-stock-management-system branch March 7, 2026 23:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant