Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions cong-no-web-handoff/tests/test_v7_inbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,8 @@ def setUp(self):
event_date="2026-04-01", existing_debt_month="2026-03-01"),
dict(fill, changed_fields=["event_date"], existing_id=8, source_row=15, container="D4", # đã có ngày: người xem
event_date="2026-03-20", existing_debt_month="2026-03-01", existing_values={"event_date": "2026-03-10"}),
dict(fill, changed_fields=["event_date"], existing_id=9, source_row=16, container="D5", # cùng tháng 3 với D1
event_date="2026-03-25", existing_debt_month="2026-03-01"),
]
self.inbox = v7_inbox.Inbox(lambda runner, ledger, actor: projection(sheet_sync={"items": [], "removed_items": []}),
lambda: (str(self.path),))
Expand All @@ -403,7 +405,7 @@ def tearDown(self):

def test_summary_counts_fill_as_machine_text_work(self):
book = v7_inbox.summarize(1, projection(sheet_sync={"items": self.items, "removed_items": []}))
self.assertEqual((book["text_diff"], book["trips_check"]["count"]), (2, 2))
self.assertEqual((book["text_diff"], book["trips_check"]["count"]), (3, 2))

def test_fills_date_only_for_undated_same_month_rows(self):
with patch.object(v7_inbox, "sheet_projection", return_value={"snapshot_sha256": "e" * 64, "items": self.items}), \
Expand All @@ -413,16 +415,37 @@ def test_fills_date_only_for_undated_same_month_rows(self):
c[1].get("month"), c[1].get("locked"), c[1]["expected_version"])
for c in self.calls if c[0] != "get_ledger"]
self.assertEqual(writes, [
("set_month_lock", None, None, None, "2026-03-01", False, 3), # tháng 3 khóa
("edit_legacy_trip", 5, "event_date", "2026-03-18", None, None, 4),
("set_month_lock", None, None, None, "2026-03-01", True, 5),
("resolve_sheet_conflict", 6, "origin", None, None, None, 6), # ô chữ như trước
("edit_legacy_trip", 6, "event_date", "2026-11-02", None, None, 7), # tháng 11 đang mở: sửa thẳng
("resolve_sheet_conflict", 6, "origin", None, None, None, 3), # ô chữ trước, không cần mở tháng
("set_month_lock", None, None, None, "2026-03-01", False, 4), # tháng 3 khóa: mở một lần
("edit_legacy_trip", 5, "event_date", "2026-03-18", None, None, 5),
("edit_legacy_trip", 9, "event_date", "2026-03-25", None, None, 6),
("set_month_lock", None, None, None, "2026-03-01", True, 7), # khóa lại một lần cho cả hai dòng
("edit_legacy_trip", 6, "event_date", "2026-11-02", None, None, 8), # tháng 11 đang mở: sửa thẳng
])
self.assertTrue(all(c[2] == v7_inbox.AUTO_ACTOR for c in self.calls))
self.assertEqual(self.inbox.auto_errors, 0)
self.assertEqual([(i["container"], i["field"], i["after"]) for i in self.inbox.auto_log if i["field"] == "event_date"],
[("D2", "event_date", "2026-11-02"), ("D1", "event_date", "2026-03-18")])
[("D2", "event_date", "2026-11-02"), ("D5", "event_date", "2026-03-25"),
("D1", "event_date", "2026-03-18")])

def test_error_in_month_relocks_once_and_keeps_earlier_fills(self):
test = self
original = self.runner.run

def failing(function, payload, actor):
if function == "edit_legacy_trip" and payload["legacy_row_id"] == 9:
test.calls.append((function, payload, actor))
raise ApiError(409, "STALE_LEDGER", "x")
return original(function, payload, actor)
self.runner.run = failing
with patch.object(v7_inbox, "sheet_projection", return_value={"snapshot_sha256": "e" * 64, "items": self.items}), \
patch.dict(os.environ, {"WEBDEBIT_AUTO_SHEET": "text"}):
self.inbox._auto_text(self.runner, 1)
locks = [c[1]["locked"] for c in self.calls if c[0] == "set_month_lock"]
self.assertEqual(locks, [False, True]) # lỗi giữa tháng: vẫn khóa lại, đúng một lần
self.assertFalse([c for c in self.calls if c[0] == "edit_legacy_trip" and c[1]["legacy_row_id"] == 6]) # dừng sổ
self.assertEqual(self.inbox.auto_errors, 1)
self.assertEqual([i["container"] for i in self.inbox.auto_log if i["field"] == "event_date"], ["D1"])


class ApiInboxRoute(unittest.TestCase):
Expand Down
37 changes: 22 additions & 15 deletions cong-no-web-handoff/web/v7_inbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,9 @@ def _auto_text(self, runner, customer_id: int) -> None:
"""Lấy theo Sheet mọi ô chữ lệch của một sổ, bằng đúng lệnh giải quyết lệch người dùng bấm (decision use_sheet).

Chỉ dòng lệch toàn ô chữ (AUTO_FIELDS), cộng ngày của dòng sổ gốc không ngày cùng tháng nợ (`date_fill`, sửa bằng
`edit_legacy_trip`, tháng khóa thì máy mở rồi khóa lại); lệch ngày khác, container, booking, khách khác để người quyết.
Mỗi ô một khóa chống ghi trùng cố định theo (bản chụp, dòng Sheet, đích, ô, giá trị Sheet). Lỗi ở một sổ dừng sổ đó,
lượt sau thử lại.
`edit_legacy_trip`); lệch ngày khác, container, booking, khách khác để người quyết. Ngày điền gom theo tháng: tháng khóa
chỉ mở rồi khóa lại một lần cho mọi dòng của tháng đó. Mỗi ô một khóa chống ghi trùng cố định theo (bản chụp, dòng
Sheet, đích, ô, giá trị Sheet). Lỗi ở một sổ dừng sổ đó, lượt sau thử lại.
"""
try:
ledger = runner.run("get_ledger", {"customer_id": customer_id}, AUTO_ACTOR)
Expand All @@ -241,6 +241,7 @@ def _auto_text(self, runner, customer_id: int) -> None:
self.auto_errors += 1
return
version, digest = ledger.get("version"), projection.get("snapshot_sha256")
fills: dict[str, list[tuple[dict[str, Any], str]]] = {}
for item in projection.get("items") or []:
fields = item.get("changed_fields") or []
fill = date_fill(item)
Expand Down Expand Up @@ -271,21 +272,27 @@ def _auto_text(self, runner, customer_id: int) -> None:
"field": field, "before": target_value, "after": sheet_value.strip()})
if fill:
day = str(item["event_date"])[:10]
key = f"auto:{digest}:{item.get('source_row')}:legacy:{item.get('existing_id')}:event_date:{day}"

def edit(at, item=item, day=day, key=key):
return runner.run("edit_legacy_trip", {
fills.setdefault(day[:7], []).append((item, day))
# Gom theo tháng (đo 3/11/2026: điền từng dòng thì 57 dòng thành 76 cặp mở/khóa trên 28 sổ–tháng, Lịch sử rối).
for month in sorted(fills):
def edit_all(at, rows=fills[month]):
for item, day in rows:
key = f"auto:{digest}:{item.get('source_row')}:legacy:{item.get('existing_id')}:event_date:{day}"
at = runner.run("edit_legacy_trip", {
"customer_id": customer_id, "expected_version": at,
"idempotency_key": hashlib.sha256(key.encode()).hexdigest(), "legacy_row_id": item.get("existing_id"),
"field": "event_date", "value": day}, AUTO_ACTOR).get("version", at)
try:
version = self._in_month(runner, customer_id, version, ledger, day[:7], key, edit)
except ApiError:
self.auto_errors += 1
return
with self.lock:
self.auto_log.appendleft({"at": self.clock(), "customer_id": customer_id, "container": item.get("container"),
"field": "event_date", "before": None, "after": day})
with self.lock:
self.auto_log.appendleft({"at": self.clock(), "customer_id": customer_id,
"container": item.get("container"), "field": "event_date",
"before": None, "after": day})
return at
try:
version = self._in_month(runner, customer_id, version, ledger, month,
f"auto:{digest}:{customer_id}:event_date", edit_all)
except ApiError:
self.auto_errors += 1
return

def _in_month(self, runner, customer_id: int, version: int, ledger: dict[str, Any], month: str, key: str,
action: Callable[[int], int]) -> int:
Expand Down
Loading