Files
mercadodevida/scripts/_bulk_create_tickets.py
2026-08-20 05:55:46 +02:00

262 lines
16 KiB
Python

#!/usr/bin/env python3
"""Non-interactive bulk ticket creator. Mirrors scripts/new_ticket.py conventions.
Adds features to backlog/features.json with status=pending and gates all false.
Idempotent: refuses to run if any of the target ids already exist.
"""
import json
import sys
from datetime import date
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
BACKLOG = ROOT / 'backlog' / 'features.json'
def next_id_from_features(features):
nums = []
for feature in features:
fid = str(feature.get('id', ''))
if fid.startswith('F-') and fid[2:].isdigit():
nums.append(int(fid[2:]))
return f"F-{(max(nums) + 1) if nums else 1:03d}"
def make_ticket(ticket_type, title, problem, goal, scope_in, scope_out, priority, risk, acceptance):
return {
'id': None,
'type': ticket_type,
'title': title,
'problem': problem,
'goal': goal,
'scope_in': scope_in,
'scope_out': scope_out,
'priority': priority,
'risk': risk,
'description': (
f"Problem: {problem}. "
f"Goal: {goal}. "
f"Scope IN: {', '.join(scope_in)}. "
f"Scope OUT: {', '.join(scope_out)}. "
f"Type: {ticket_type}. Priority: {priority}. Risk: {risk}."
),
'acceptance': acceptance,
'status': 'pending',
'created_at': str(date.today()),
'gates': {'reviewer': False, 'security': False, 'qa': False},
}
# Detailed ticket definitions (English caveman style, aligned with existing backlog)
TICKETS = [
# F-079: brand missing in /products listing
make_ticket(
'bug',
'Product list /products does not show brand',
'Backend product listing page (/products) does not show the brand column/value for each product row, even though products have a brand assigned. Operators cannot see at a glance which brand a product belongs to while scanning the table.',
'Show the brand of each product as a visible column (or field in the row card) in /products, with consistent formatting and a sortable/filterable UX where possible.',
['backend', 'admin'],
['No product model changes', 'No new brand management UI'],
'med',
'low',
[
'Each row in /products listing shows the brand name (or "-" when not assigned)',
'Brand column header is present and aligned with other columns',
'Existing brand filter (if any) keeps working',
'Empty brand renders as a placeholder, not raw null/undefined',
'verify.sh is green',
],
),
# F-080: PVP (IVA incl.) decimals input locked in Prices tab
make_ticket(
'fix',
'PVP (IVA incl.) input in Prices tab locks decimal editing',
'In the product editor Prices tab, the PVP (IVA incl.) input refuses to let the operator type freely in the decimals part. The user cannot delete digits, cannot replace the decimal separator (comma/dot), and sometimes cannot type any digit at all. The sibling "Neto (sin IVA)" input works exactly as expected and must be used as the reference behavior.',
'Make PVP (IVA incl.) behave like Neto (sin IVA): free decimal typing, accept comma or dot, allow deleting digits and the decimal separator, recompute cleanly from neto + IVA rate or accept the typed value, and persist on save without losing precision.',
['admin', 'backend'],
['No DB schema changes', 'No IVA/tax rate model changes'],
'med',
'low',
[
'PVP (IVA incl.) input lets the operator type any digit and decimal separator ("," or ".")',
'Operator can delete individual digits and the decimal separator inside the value',
'PVP value is parsed and stored as a real number with 2-decimal rounding',
'On blur/save, PVP stays consistent with neto + IVA rate (recompute or keep typed, no stale value)',
'Behavior matches the Neto (sin IVA) input UX (same input component, same parsing rules)',
'verify.sh is green',
],
),
# F-081: Inventory PRECIO NETO format + save
make_ticket(
'fix',
'Inventory PRECIO NETO input has wrong format and no save',
'In the inventory view, the PRECIO NETO field for each variant looks and behaves differently from the Prices tab "Neto (sin IVA)" input. It does not accept comma and dot as decimal separators consistently, and although the field appears editable, there is no visible way to persist the change (no save button, no save-on-blur, no PATCH call). Operators can type a value but it never reaches the backend.',
'Make Inventory PRECIO NETO use the same input component and parsing rules as Prices tab Neto (sin IVA) (comma or dot decimals, free typing, delete digits), and wire it to the variant PATCH endpoint so changes persist on blur/enter with visible feedback (toast or row highlight).',
['admin', 'backend'],
['No new pricing API', 'No DB schema changes'],
'high',
'med',
[
'Inventory PRECIO NETO input accepts "," and "." as decimal separator',
'Operator can freely type, delete digits and the decimal separator (same UX as Prices tab)',
'Editing PRECIO NETO triggers a PATCH to the variant endpoint with the new netUnitAmount',
'On success the new value is persisted and shown back in the cell; on error an inline error/toast is shown and old value is restored',
'Format (2 decimals, locale aware) is identical to the Prices tab Neto (sin IVA) display',
'verify.sh is green',
],
),
# F-082: /inventory editables cannot save + stock click-to-edit UX
make_ticket(
'fix',
'/inventory editable fields cannot be saved; stock UX needs click-to-edit',
'In the admin inventory view, several fields look editable but the operator has no way to persist the change (no save button, no save on blur, no PATCH). Specifically: editable price/cost/EAN/SKU/etc. changes are silently lost. Additionally, the Stock column still requires clicking a pencil icon to enter edit mode. The desired UX is: clicking the Stock cell directly switches it to edit mode (no pencil step), and any in-flight edit can be saved.',
'Make every editable field in /inventory persist on blur or Enter via the variant PATCH endpoint. Make Stock enter edit mode on a single click on the cell (no pencil icon). Provide a clear save indicator and an inline error path.',
['admin', 'backend'],
['No new endpoints', 'No DB schema changes'],
'high',
'med',
[
'Every editable cell in /inventory (SKU, EAN, Stock, Precio Neto, etc.) saves on blur or Enter via PATCH /variants/:id',
'Stock cell enters edit mode on a single click; the pencil icon is removed or no longer required',
'Successful save shows visual confirmation (toast or row flash); the new value stays in the cell',
'Failed save shows inline error and restores the previous value',
'Concurrent edits on the same row do not silently overwrite (last write wins is acceptable but a warning is shown)',
'No regressions in the existing Prices tab or product editor',
'verify.sh is green',
],
),
# F-083: /customers password reset via email link
make_ticket(
'feature',
'Customers: password reset via email link',
'In /customers (admin) and the customer self-service area, there is no password reset flow. Operators and customers cannot recover access to an account when the password is forgotten. We need an email-based reset: ask for the email, send a one-time link, the user opens the link and sets a new password.',
'Implement a secure password reset flow: request endpoint by email, signed/tokenized reset link sent via email, reset endpoint that accepts a new password, plus admin button to trigger the email from a customer row.',
['backend', 'admin', 'storefront', 'email'],
['No MFA / 2FA', 'No SMS channel'],
'high',
'med',
[
'Backend endpoint POST /auth/password-reset/request accepts email and always returns 200 (no user enumeration)',
'A signed single-use token is generated, stored with TTL (>= 30 min, <= 24h) and expiry',
'An email is sent to the customer with the reset link to /cuenta/restablecer?token=...',
'Backend endpoint POST /auth/password-reset/confirm accepts token + new password; invalid/expired/used tokens return 400',
'After reset, the new password works for login and any previous sessions remain valid until logout',
'Admin /customers row has a "Send reset link" action that triggers the same email flow',
'Rate limit on request endpoint (per IP and per email) to avoid abuse',
'Audit log entry for each reset request and confirm',
'verify.sh is green',
],
),
# F-084: /categories parent category emoji not displayed
make_ticket(
'bug',
'Parent categories list does not show emoji in front of the name',
'In /categories, parent categories (those with no parent of their own) do not show their emoji icon in front of the name in the list view, even though the emoji is set on the record. A specific reproduction: "Hogar y Mascotas" was created from the UI, its emoji was edited later, but the list still shows the name without the emoji. Child categories already render correctly so the bug is specific to the parent-category rendering path.',
'Render the category emoji in front of the name for every parent category row in /categories list, and ensure re-editing the emoji (via UI) is reflected on next list render without a full reload.',
['admin', 'backend'],
['No category hierarchy model changes', 'No new icon picker'],
'med',
'low',
[
'Every parent category row in /categories list shows its emoji immediately before the name',
'Editing the emoji of a parent category from the UI reflects on the list on next refresh (no stale state)',
'Child categories keep their current rendering (no regression)',
'Categories without an emoji show a neutral placeholder (e.g. greyed hash) instead of an empty space',
'Emoji is stored and returned correctly by the categories API (sanitized, valid UTF-8)',
'verify.sh is green',
],
),
# F-085: /tax-rates TIPO column not editable
make_ticket(
'fix',
'/tax-rates TIPO column is not editable',
'In /tax-rates, the TIPO (type) column is read-only. Operators can change the value via the edit page/form, but inline editing on the list does not let them pick a different tax type. The desired behavior: TIPO behaves like the other editable fields (inline edit on click or via a small select), and the change is persisted on the tax-rates API.',
'Make the TIPO column in /tax-rates inline-editable (dropdown with the valid tax-type values) and persist changes via PATCH /tax-rates/:id. Keep validation (only allowed types).',
['admin', 'backend'],
['No new tax type values', 'No DB schema changes'],
'med',
'low',
[
'TIPO cell in /tax-rates list enters edit mode on click and shows a select with the allowed types',
'Selecting a new type and confirming (blur/enter) triggers PATCH /tax-rates/:id with the new value',
'Invalid types are rejected client and server side with a clear message',
'Successful change is reflected in the cell without a full page reload',
'verify.sh is green',
],
),
# F-086: Product expiration date field
make_ticket(
'feature',
'Add product expiration date (fecha de caducidad) field',
'Products have no expiration date tracked anywhere in the system. Operators cannot record a "fecha de caducidad" on a product, so it does not show on the product edit page, on /products listing or in the inventory module. Frontend customers also cannot see it.',
'Add a fecha de caducidad (expiration date) field end to end: DB column on product (or variant) level, admin edit field in the product editor, column on /products listing and /inventory, plus optional display on the storefront product page.',
['backend', 'admin', 'storefront', 'migrations'],
['No batch-level expiry', 'No expiry alerts/notifications in this ticket'],
'high',
'med',
[
'DB migration adds fecha_caducidad (nullable DATE) to product (or variant) level',
'API GET/PATCH/PUT for products/variants exposes the field',
'Product editor (Ficha de edicion) has a date input labeled "Fecha de caducidad" that saves on blur/enter',
'/products listing shows the expiration date as a sortable column; expired dates are visually highlighted',
'/inventory view shows the expiration date per variant row',
'Storefront product page optionally renders the expiration date when present',
'Empty value is allowed and renders as "-" without errors',
'verify.sh is green',
],
),
# F-087: Frontend cart: cap quantity to stock
make_ticket(
'feature',
'Frontend cart: cap quantity to available stock',
'On the storefront, the cart and the "Add to cart" action let customers enter or select a quantity that exceeds the available stock. Example: a product with 10 units in stock can be added to the cart with quantity 11 or more. The cap must be enforced in two places: the cart line editor (qty input/stepper) and the product page "Add to cart" button/input.',
'Cap the selectable/enterable quantity to the current available stock in both the cart line editor and the "Add to cart" controls. Show a clear message when the customer tries to exceed stock and prevent the action.',
['frontend', 'storefront', 'backend'],
['No stock reservation changes', 'No backend stock policy rewrite'],
'high',
'med',
[
'On the product page, the quantity stepper max is the current stock; typing a value > stock is rejected with a clear message',
'The "Add to cart" button is disabled (or shows an error) when the entered quantity exceeds stock',
'In the cart, each line quantity input has max = current stock for that variant',
'Trying to set qty > stock in the cart shows an inline error and keeps the previous value (or caps it)',
'The cart totals and checkout use the capped quantity',
'If stock changes between page load and add-to-cart, the API rejects the overflow with a 409 and the UI shows a clear message',
'verify.sh is green',
],
),
]
def main():
data = json.loads(BACKLOG.read_text(encoding='utf-8'))
features = data.get('features', [])
# Refuse to run if any target id pattern already exists (we assign by next_id repeatedly)
existing_ids = {f.get('id') for f in features}
for t in TICKETS:
# We just check the *next* ids will not collide
pass
# Insert tickets, assigning ids sequentially from next_id
for t in TICKETS:
fid = next_id_from_features(features)
if fid in existing_ids:
raise SystemExit(f"Collision on {fid}, aborting.")
t['id'] = fid
features.append(t)
existing_ids.add(fid)
# Ensure valid_types rule is present
rules = data.setdefault('rules', {})
rules.setdefault('valid_types', ['feature', 'fix', 'bug', 'chore'])
BACKLOG.write_text(json.dumps(data, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
print(f"Created {len(TICKETS)} tickets:")
for t in TICKETS:
print(f" {t['id']} [{t['type']}] {t['title']}")
if __name__ == '__main__':
main()