From 7cb95aee837d86d33e94bc0ef5051c30b0860d5d Mon Sep 17 00:00:00 2001 From: Ittipat Lusuk Date: Wed, 17 Jun 2026 11:32:41 +0700 Subject: [PATCH] [sheet-service] Management price & price slot, set config for any country --- main.py | 584 ++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 394 insertions(+), 190 deletions(-) diff --git a/main.py b/main.py index 1eb659c..86ca390 100644 --- a/main.py +++ b/main.py @@ -69,32 +69,105 @@ COUNTRY_MAPPING = { "ltu": { "spreadsheet_id": os.getenv("SPREAD_SHEET_ID_LTU"), "sheets": { + "new-layout-v2": os.getenv("SHEET_NEW_LAYOUT_V2_LTU"), + "name-desc-v2": os.getenv("SHEET_NAME_DESC_V2_LTU"), "price": os.getenv("SHEET_PRICE_LTU") }, "grist_doc_id": { + "new-layout-v2": os.getenv("DOC_ID_NEW_LAYOUT_V2_LTU"), + "name-desc-v2": os.getenv("DOC_ID_NAME_DESC_V2_LTU"), "price": os.getenv("DOC_ID_PRICE_LTU") } }, + "rou": { + "spreadsheet_id": os.getenv("SPREAD_SHEET_ID_ROU"), + "sheets": { + "new-layout-v2": os.getenv("SHEET_NEW_LAYOUT_V2_ROU"), + "name-desc-v2": os.getenv("SHEET_NAME_DESC_V2_ROU"), + "price": os.getenv("SHEET_PRICE_ROU") + }, + "grist_doc_id": { + "new-layout-v2": os.getenv("DOC_ID_NEW_LAYOUT_V2_ROU"), + "name-desc-v2": os.getenv("DOC_ID_NAME_DESC_V2_ROU"), + "price": os.getenv("DOC_ID_PRICE_ROU") + } + }, "hkg": { "spreadsheet_id": os.getenv("SPREAD_SHEET_ID_HKG"), "sheets": { "new-layout-v2": os.getenv("SHEET_NEW_LAYOUT_V2_HKG"), - "name-desc-v2": os.getenv("SHEET_NAME_DESC_V2_HKG") + "name-desc-v2": os.getenv("SHEET_NAME_DESC_V2_HKG"), + "price": os.getenv("SHEET_PRICE_HKG") }, "grist_doc_id": { "new-layout-v2": os.getenv("DOC_ID_NEW_LAYOUT_V2_HKG"), - "name-desc-v2": os.getenv("DOC_ID_NAME_DESC_V2_HKG") + "name-desc-v2": os.getenv("DOC_ID_NAME_DESC_V2_HKG"), + "price": os.getenv("DOC_ID_PRICE_HKG") } }, "sgp": { "spreadsheet_id": os.getenv("SPREAD_SHEET_ID_SGP"), "sheets": { "new-layout-v2": os.getenv("SHEET_NEW_LAYOUT_V2_SGP"), - "name-desc-v2": os.getenv("SHEET_NAME_DESC_V2_SGP") + "name-desc-v2": os.getenv("SHEET_NAME_DESC_V2_SGP"), + "price": os.getenv("SHEET_PRICE_SGP") }, "grist_doc_id": { "new-layout-v2": os.getenv("DOC_ID_NEW_LAYOUT_V2_SGP"), - "name-desc-v2": os.getenv("DOC_ID_NAME_DESC_V2_SGP") + "name-desc-v2": os.getenv("DOC_ID_NAME_DESC_V2_SGP"), + "price": os.getenv("DOC_ID_PRICE_SGP") + } + }, + "gbr": { + "spreadsheet_id": os.getenv("SPREAD_SHEET_ID_GBR"), + "sheets": { + "new-layout-v2": os.getenv("SHEET_NEW_LAYOUT_V2_GBR"), + "name-desc-v2": os.getenv("SHEET_NAME_DESC_V2_GBR"), + "price": os.getenv("SHEET_PRICE_GBR") + }, + "grist_doc_id": { + "new-layout-v2": os.getenv("DOC_ID_NEW_LAYOUT_V2_GBR"), + "name-desc-v2": os.getenv("DOC_ID_NAME_DESC_V2_GBR"), + "price": os.getenv("DOC_ID_PRICE_GBR") + } + }, + "aus": { + "spreadsheet_id": os.getenv("SPREAD_SHEET_ID_AUS"), + "sheets": { + "new-layout-v2": os.getenv("SHEET_NEW_LAYOUT_V2_AUS"), + "name-desc-v2": os.getenv("SHEET_NAME_DESC_V2_AUS"), + "price": os.getenv("SHEET_PRICE_AUS") + }, + "grist_doc_id": { + "new-layout-v2": os.getenv("DOC_ID_NEW_LAYOUT_V2_AUS"), + "name-desc-v2": os.getenv("DOC_ID_NAME_DESC_V2_AUS"), + "price": os.getenv("DOC_ID_PRICE_AUS") + } + }, + "uae_dubai": { + "spreadsheet_id": os.getenv("SPREAD_SHEET_ID_UAE_DUBAI"), + "sheets": { + "new-layout-v2": os.getenv("SHEET_NEW_LAYOUT_V2_UAE_DUBAI"), + "name-desc-v2": os.getenv("SHEET_NAME_DESC_V2_UAE_DUBAI"), + "price": os.getenv("SHEET_PRICE_UAE_DUBAI") + }, + "grist_doc_id": { + "new-layout-v2": os.getenv("DOC_ID_NEW_LAYOUT_V2_UAE_DUBAI"), + "name-desc-v2": os.getenv("DOC_ID_NAME_DESC_V2_UAE_DUBAI"), + "price": os.getenv("DOC_ID_PRICE_UAE_DUBAI") + } + }, + "mys": { + "spreadsheet_id": os.getenv("SPREAD_SHEET_ID_MYS"), + "sheets": { + "new-layout-v2": os.getenv("SHEET_NEW_LAYOUT_V2_MYS"), + "name-desc-v2": os.getenv("SHEET_NAME_DESC_V2_MYS"), + "price": os.getenv("SHEET_PRICE_MYS") + }, + "grist_doc_id": { + "new-layout-v2": os.getenv("DOC_ID_NEW_LAYOUT_V2_MYS"), + "name-desc-v2": os.getenv("DOC_ID_NAME_DESC_V2_MYS"), + "price": os.getenv("DOC_ID_PRICE_MYS") } } } @@ -278,6 +351,7 @@ def redis_message_handler(): catalog = values.get("catalog", "") catalog_name = values.get("catalog_name", "") content = values.get("content", []) + option = values.get("option", "") srv_name = payload.get("srv_name", "") user_id = user_info.get("uid") @@ -317,7 +391,7 @@ def redis_message_handler(): print(f"[{SERVICE_NAME}] Heartbeat Failed: {catalog} | User: {user_id}") continue - print(f"[{SERVICE_NAME}] Heartbeat success: {catalog} | User: {user_id}") + # print(f"[{SERVICE_NAME}] Heartbeat success: {catalog} | User: {user_id}") elif channel == EXIT_CHANNEL: if not catalog: @@ -355,7 +429,7 @@ def redis_message_handler(): if res.status_code == 200: print(f"[{SERVICE_NAME}] Get catalog success | User: {user_id}") - print(payload) + # print(payload) except Exception as e: @@ -386,13 +460,14 @@ def redis_message_handler(): continue try: + print(f"[{SERVICE_NAME}] Add menu received | User: {user_id} | Content: {content}") handle_add_menu(country, catalog, content) - print(f"[{SERVICE_NAME}] Add menu success: {catalog} | User: {user_id}") + # print(f"[{SERVICE_NAME}] Add menu success: {catalog} | User: {user_id}") except Exception as e: print(f"[{SERVICE_NAME}] Add menu error: {e}") elif channel == ADD_CATALOG_CHANNEL: - if not (catalog_name, catalog): + if not (catalog_name and catalog): print(f"[{SERVICE_NAME}] Missing required parameters | Channel: {channel} | User: {user_id}") continue @@ -448,13 +523,15 @@ def redis_message_handler(): traceback.print_exc() elif channel == GET_PRICE_CHANNEL: - if not (content): - print(f"[{SERVICE_NAME}] Missing required parameters | Channel: {channel}") - continue - try: - handle_get_price(country, user_id, content) - print(f"[{SERVICE_NAME}] Get price menu success: {catalog} | User: {user_id}") + if (option and option == "PriceSlot"): + handle_get_price_slot(country, user_id) + else: + if not (content): + raise Exception (f"[{SERVICE_NAME}] Missing required parameters | Channel: {channel}") + + handle_get_price(country, user_id, content) + # print(f"[{SERVICE_NAME}] Get price menu success: {catalog} | User: {user_id}") except Exception as e: print(f"[{SERVICE_NAME}] Get price menu error: {e}") @@ -464,8 +541,7 @@ def redis_message_handler(): continue try: - handle_add_price(country, content) - print(f"[{SERVICE_NAME}] Add price menu success: {catalog} | User: {user_id}") + handle_add_price(country, content, option) except Exception as e: print(f"[{SERVICE_NAME}] Add price menu error: {e}") @@ -475,8 +551,7 @@ def redis_message_handler(): continue try: - handle_update_price(country, content) - print(f"[{SERVICE_NAME}] Update price menu success: {catalog} | User: {user_id}") + handle_update_price(country, content, option) except Exception as e: print(f"[{SERVICE_NAME}] Update price menu error: {e}") @@ -486,7 +561,7 @@ def redis_message_handler(): continue try: - handle_delete_price(country, content) + handle_delete_price(country, content, option) print(f"[{SERVICE_NAME}] Delete price menu success: {catalog} | User: {user_id}") except Exception as e: print(f"[{SERVICE_NAME}] Delete price menu error: {e}") @@ -524,7 +599,7 @@ def find_grist_table_id(doc_id, catalog_suffix): print(f"Error: {e}") return None -def send_stream_notification(msg_type: str, content: any, batch_id: str, current_chunk: int, total_chunks: int, total_items: int, user_id: str): +def send_stream_notification(msg_type: str, content: any, batch_id: str, current_chunk: int, total_chunks: int, total_items: int, user_id: str, ref: str): """ msg_type: "start", "chunk", "end", "error" """ @@ -543,7 +618,7 @@ def send_stream_notification(msg_type: str, content: any, batch_id: str, current "total_chunks": total_chunks, "total_items": total_items, "to": user_id, - "ref": "catalog", + "ref": ref, "content": content } } @@ -565,7 +640,7 @@ def send_stream_notification(msg_type: str, content: any, batch_id: str, current def process_and_stream_sheet_data(country: str, catalog: str, user_id: str): batch_id = str(uuid.uuid4()) - send_stream_notification("start", {"message": f"Start fetching catalog: {catalog}"}, batch_id, 0, 0, 0, user_id) + send_stream_notification("start", {"message": f"Start fetching catalog: {catalog}"}, batch_id, 0, 0, 0, user_id, "catalog") try: config = COUNTRY_MAPPING.get(country) @@ -665,13 +740,14 @@ def process_and_stream_sheet_data(country: str, catalog: str, user_id: str): start_idx = i * CHUNK_SIZE end_idx = start_idx + CHUNK_SIZE chunk_data = final_result[start_idx:end_idx] - send_stream_notification("chunk", chunk_data, batch_id, i + 1, total_chunks, total_items, user_id) + send_stream_notification("chunk", chunk_data, batch_id, i + 1, total_chunks, total_items, user_id, "catalog") - send_stream_notification("end", {"message": "All data sent successfully"}, batch_id, total_chunks, total_chunks, total_items, user_id) + send_stream_notification("end", {"message": "All data sent successfully"}, batch_id, total_chunks, total_chunks, total_items, user_id, "catalog") except Exception as e: traceback.print_exc() - send_stream_notification("error", {"error_detail": str(e)}, batch_id, 0, 0, 0, user_id) + send_stream_notification("error", {"error_detail": str(e)}, batch_id, 0, 0, 0, user_id, "catalog") + raise Exception (f"Stream data got error: {e}") # get catalog def get_catalogs(country: str): @@ -750,156 +826,160 @@ def to_grist_record(row_list): return {"fields": {chr(65 + i): val for i, val in enumerate(row_list)}} def handle_add_menu(country: str, catalog: str, content: list): - config = COUNTRY_MAPPING.get(country) - if not config: - print(f"[{SERVICE_NAME}] Country {country} config not found") - return + try: + config = COUNTRY_MAPPING.get(country) + if not config: + print(f"[{SERVICE_NAME}] Country {country} config not found") + return - grist_docs = config.get("grist_doc_id", {}) - has_nl = "new-layout" in grist_docs + grist_docs = config.get("grist_doc_id", {}) + has_nl = "new-layout" in grist_docs - doc_nl = grist_docs.get("new-layout") - doc_nv2 = grist_docs.get("new-layout-v2") - doc_nd = grist_docs.get("name-desc-v2") + doc_nl = grist_docs.get("new-layout") + doc_nv2 = grist_docs.get("new-layout-v2") + doc_nd = grist_docs.get("name-desc-v2") - nl_table_id = find_grist_table_id(doc_nl, catalog) if has_nl else None - nv2_table_id = find_grist_table_id(doc_nv2, catalog) - nd_table_id = "Name_desc_v2" + nl_table_id = find_grist_table_id(doc_nl, catalog) if has_nl else None + nv2_table_id = find_grist_table_id(doc_nv2, catalog) + nd_table_id = "Name_desc_v2" - if has_nl and not nl_table_id: - print(f"[{SERVICE_NAME}] Table for {catalog} not found in doc NL") - return - if not nv2_table_id: - print(f"[{SERVICE_NAME}] Table for {catalog} not found in doc NV2") - return + if has_nl and not nl_table_id: + print(f"[{SERVICE_NAME}] Table for {catalog} not found in doc NL") + return + if not nv2_table_id: + print(f"[{SERVICE_NAME}] Table for {catalog} not found in doc NV2") + return - def norm_val(v): return v if v not in [None, ""] else "-" + def norm_val(v): return v if v not in [None, ""] else "-" - nd_existing_data = fetch_grist_table_data(doc_nd, nd_table_id) - existing_keys = set() - if nd_existing_data: - for row_obj in nd_existing_data: - row = row_obj["fields"] - key = row[0] - if key: - existing_keys.add(key) - - # for THA - existing_nl_codes = set() - if has_nl and nl_table_id: - nl_data = fetch_grist_table_data(doc_nl, nl_table_id) - if nl_data: - for row_obj in nl_data: + nd_existing_data = fetch_grist_table_data(doc_nd, nd_table_id) + existing_keys = set() + if nd_existing_data: + for row_obj in nd_existing_data: row = row_obj["fields"] - codes = tuple(row[i] if i < len(row) else "-" for i in [6, 7, 8, 10, 11, 12]) - existing_nl_codes.add(codes) + key = row[0] + if key: + existing_keys.add(key) - nv2_data = fetch_grist_table_data(doc_nv2, nv2_table_id) - existing_nv2_codes = set() - if nv2_data: - for row_obj in nv2_data: - row = row_obj["fields"] - is_name = row[1] if len(row) > 1 else "" - if is_name == "name": - codes = (norm_val(row[8] if len(row) > 8 else "-"), - norm_val(row[9] if len(row) > 9 else "-"), - norm_val(row[10] if len(row) > 10 else "-")) - existing_nv2_codes.add(codes) + # for THA + existing_nl_codes = set() + if has_nl and nl_table_id: + nl_data = fetch_grist_table_data(doc_nl, nl_table_id) + if nl_data: + for row_obj in nl_data: + row = row_obj["fields"] + codes = tuple(row[i] if i < len(row) else "-" for i in [6, 7, 8, 10, 11, 12]) + existing_nl_codes.add(codes) - nl_records = [] - nv2_records = [] - nd_records = [] + nv2_data = fetch_grist_table_data(doc_nv2, nv2_table_id) + existing_nv2_codes = set() + if nv2_data: + for row_obj in nv2_data: + row = row_obj["fields"] + is_name = row[1] if len(row) > 1 else "" + if is_name == "name": + codes = (norm_val(row[8] if len(row) > 8 else "-"), + norm_val(row[9] if len(row) > 9 else "-"), + norm_val(row[10] if len(row) > 10 else "-")) + existing_nv2_codes.add(codes) - for item in content: - cells = item.get("cells", []) - if not cells: - continue - - payload_lang = item.get("payload", {}) - lang_name = payload_lang.get("lang_name", ["", "", "", ""]) - lang_desc = payload_lang.get("lang_desc", ["", "", "", ""]) + nl_records = [] + nv2_records = [] + nd_records = [] - # ========================================== - # New-Layout (THA) - # ========================================== - if has_nl: - current_nl_tuple = tuple(norm_val(get_val(cells, i)) for i in [6,7,8,10,11,12]) - if current_nl_tuple not in existing_nl_codes: - nl_records.append(to_grist_record(cells)) - existing_nl_codes.add(current_nl_tuple) + for item in content: + cells = item.get("cells", []) + if not cells: + continue + + payload_lang = item.get("payload", {}) + lang_name = payload_lang.get("lang_name", ["", "", "", ""]) + lang_desc = payload_lang.get("lang_desc", ["", "", "", ""]) - # ========================================== - # New-Layout-V2 (All country) - # ========================================== - hot_code = f"{get_val(cells, 6)},{get_val(cells, 10)}" - cold_code = f"{get_val(cells, 7)},{get_val(cells, 11)}" - blend_code = f"{get_val(cells, 8)},{get_val(cells, 12)}" - current_nv2_tuple = (hot_code, cold_code, blend_code) + # ========================================== + # New-Layout (THA) + # ========================================== + if has_nl: + current_nl_tuple = tuple(norm_val(get_val(cells, i)) for i in [6,7,8,10,11,12]) + if current_nl_tuple not in existing_nl_codes: + nl_records.append(to_grist_record(cells)) + existing_nl_codes.add(current_nl_tuple) - if current_nv2_tuple not in existing_nv2_codes: - name_row = ["", "name", get_val(cells, 3), get_val(cells, 2), get_val(lang_name, 0), get_val(lang_name, 1), get_val(lang_name, 2), get_val(lang_name, 3), - hot_code, cold_code, blend_code, "", "", "", "", "", "", "", - get_val(cells, 14), get_val(cells, 15), get_val(cells, 16), get_val(cells, 17), get_val(cells, 18)] - - desc_row = ["", "desc", get_val(cells, 5), get_val(cells, 4), get_val(lang_desc, 0), get_val(lang_desc, 1), get_val(lang_desc, 2), get_val(lang_desc, 3), - "||||||||||||||||||||||||||", "||||||||||||||||||||||||||", "||||||||||||||||||||||||||", "", "", "", "", "", "", "", + # ========================================== + # New-Layout-V2 (All country) + # ========================================== + hot_code = f"{get_val(cells, 6)},{get_val(cells, 10)}" + cold_code = f"{get_val(cells, 7)},{get_val(cells, 11)}" + blend_code = f"{get_val(cells, 8)},{get_val(cells, 12)}" + current_nv2_tuple = (hot_code, cold_code, blend_code) + + if current_nv2_tuple not in existing_nv2_codes: + name_row = ["", "name", get_val(cells, 3), get_val(cells, 2), get_val(lang_name, 0), get_val(lang_name, 1), get_val(lang_name, 2), get_val(lang_name, 3), + hot_code, cold_code, blend_code, "", "", "", "", "", "", "", + get_val(cells, 14), get_val(cells, 15), get_val(cells, 16), get_val(cells, 17), get_val(cells, 18)] + + desc_row = ["", "desc", get_val(cells, 5), get_val(cells, 4), get_val(lang_desc, 0), get_val(lang_desc, 1), get_val(lang_desc, 2), get_val(lang_desc, 3), + "||||||||||||||||||||||||||", "||||||||||||||||||||||||||", "||||||||||||||||||||||||||", "", "", "", "", "", "", "", + "-", "-", "-", "-", "-"] + + img_row = ["", "img", get_val(cells, 9), "-", "-", "-", "-", "-", get_val(cells, 13), + "||||||||||||||||||||||||||", "||||||||||||||||||||||||||", "", "", "", "", "", "", "", "-", "-", "-", "-", "-"] + + blank_row = [""] * 23 + + nv2_records.extend([to_grist_record(name_row), to_grist_record(desc_row), to_grist_record(img_row), to_grist_record(blank_row)]) + existing_nv2_codes.add(current_nv2_tuple) + + # ========================================== + # Name-desc-V2 (All country) + # ========================================== + product_code_indices = [6, 7, 8, 10, 11, 12] - img_row = ["", "img", get_val(cells, 9), "-", "-", "-", "-", "-", get_val(cells, 13), - "||||||||||||||||||||||||||", "||||||||||||||||||||||||||", "", "", "", "", "", "", "", - "-", "-", "-", "-", "-"] - - blank_row = [""] * 23 + for p_idx in product_code_indices: + code = get_val(cells, p_idx) + if code == "-" or not code: + continue - nv2_records.extend([to_grist_record(name_row), to_grist_record(desc_row), to_grist_record(img_row), to_grist_record(blank_row)]) - existing_nv2_codes.add(current_nv2_tuple) + menu_name_key = f"MENU.{code}.name" + + if menu_name_key in existing_keys: + continue - # ========================================== - # Name-desc-V2 (All country) - # ========================================== - product_code_indices = [6, 7, 8, 10, 11, 12] - - for p_idx in product_code_indices: - code = get_val(cells, p_idx) - if code == "-" or not code: - continue + parts = code.split('-') + drink_type = "UNKNOWN" + drink_type_th = "" - menu_name_key = f"MENU.{code}.name" - - if menu_name_key in existing_keys: - continue + if len(parts) >= 3: + type_id = parts[2] + if type_id == "01": + drink_type = "HOT" + drink_type_th = "ร้อน" + elif type_id == "02": + drink_type = "ICED" + drink_type_th = "เย็น" + elif type_id == "03": + drink_type = "SMOOTHIE" + drink_type_th = "ปั่น" - parts = code.split('-') - drink_type = "UNKNOWN" - drink_type_th = "" + name_en = get_val(cells, 3, "") + name_th = get_val(cells, 2, "") - if len(parts) >= 3: - type_id = parts[2] - if type_id == "01": - drink_type = "HOT" - drink_type_th = "ร้อน" - elif type_id == "02": - drink_type = "ICED" - drink_type_th = "เย็น" - elif type_id == "03": - drink_type = "SMOOTHIE" - drink_type_th = "ปั่น" + if drink_type == "SMOOTHIE": + prefix = f"{name_en} {drink_type}".strip() + else: + prefix = f"{drink_type} {name_en}".strip() - name_en = get_val(cells, 3, "") - name_th = get_val(cells, 2, "") + prefix_th = f"{name_th} {drink_type_th}".strip() if drink_type_th else name_th - if drink_type == "SMOOTHIE": - prefix = f"{name_en} {drink_type}".strip() - else: - prefix = f"{drink_type} {name_en}".strip() + nd_name_row = [menu_name_key, get_val(cells, 9), prefix, prefix_th, get_val(lang_name, 0), get_val(lang_name, 1), get_val(lang_name, 2), get_val(lang_name, 3)] + nd_desc_row = [f"MENU.{code}.desc", "-", get_val(cells, 5), get_val(cells, 4), get_val(lang_desc, 0), get_val(lang_desc, 1), get_val(lang_desc, 2), get_val(lang_desc, 3)] - prefix_th = f"{name_th} {drink_type_th}".strip() if drink_type_th else name_th - - nd_name_row = [menu_name_key, get_val(cells, 9), prefix, prefix_th, get_val(lang_name, 0), get_val(lang_name, 1), get_val(lang_name, 2), get_val(lang_name, 3)] - nd_desc_row = [f"MENU.{code}.desc", "-", get_val(cells, 5), get_val(cells, 4), get_val(lang_desc, 0), get_val(lang_desc, 1), get_val(lang_desc, 2), get_val(lang_desc, 3)] - - nd_records.extend([to_grist_record(nd_name_row), to_grist_record(nd_desc_row)]) - existing_keys.add(menu_name_key) + nd_records.extend([to_grist_record(nd_name_row), to_grist_record(nd_desc_row)]) + existing_keys.add(menu_name_key) + except Exception as e: + print(f"[{SERVICE_NAME}] handle_add_menu error: {e}") + traceback.print_exc() headers = {"Authorization": f"Bearer {GRIST_API_KEY}", "Content-Type": "application/json"} @@ -915,11 +995,11 @@ def handle_add_menu(country: str, catalog: str, content: list): json={"records": records} ) if resp.status_code != 200: - print(f"Error adding to {table_id}: {resp.text}") + print(f"[{SERVICE_NAME}] Error adding to {table_id}: {resp.text}") except Exception as e: - print(f"Request failed for {table_id}: {e}") + print(f"[{SERVICE_NAME}] Request failed for {table_id}: {e}") - # NL (เฉพาะ THA) + # NL (THA) if has_nl and nl_records: add_records_to_grist(doc_nl, nl_table_id, nl_records) @@ -1654,14 +1734,23 @@ def handle_get_price(country: str, user_id: str, content: list): if res.status_code == 200: print(f"[{SERVICE_NAME}] Price data sent to user: {user_id} | Products: {len(result)}") + print(f"[{SERVICE_NAME}] Get price data to user: {user_id} | data: {payload}") + else: + # print(f"[{SERVICE_NAME}] Response error: {res}") + error_detail = res.text + try: + error_detail = res.json() + except: + pass + print(f"[{SERVICE_NAME}] Notify server response: {res.status_code} | {error_detail}") - print(result) + # print(result) return result except Exception as e: - print(f"[{SERVICE_NAME}] Get price error: {e}") - traceback.print_exc() + # print(f"[{SERVICE_NAME}] Get price error: {e}") + # traceback.print_exc() if FRONTEND_NOTIFY_URL: payload = { @@ -1682,22 +1771,117 @@ def handle_get_price(country: str, user_id: str, content: list): json=payload, ) - raise + raise Exception(f"[{SERVICE_NAME}] Get price error: {e}") +def handle_get_price_slot(country: str, user_id: str): + batch_id = str(uuid.uuid4()) + + try: + config = COUNTRY_MAPPING.get(country) + if not config: + raise Exception(f"Country {country} not found") + + grist_docs = config.get("grist_doc_id", {}) + doc_price = grist_docs.get("price") + + if not doc_price: + raise Exception(f"Price doc not found for country {country}") -def handle_add_price(country: str, content: list): + # 1. ดึงรายชื่อ Table ทั้งหมดใน Document Price + all_tables = get_all_grist_tables(doc_price) + + # 2. ค้นหา Table ที่เข้าเงื่อนไข PriceSlot[index] + price_slot_pattern = re.compile(r"^PriceSlot[1-9]\d*$") + matching_tables = [] + + for t_id in all_tables: + table_name = reconstruct_table_name(t_id) + if price_slot_pattern.match(table_name) or price_slot_pattern.match(t_id): + matching_tables.append({ + "id": t_id, + "name": table_name if price_slot_pattern.match(table_name) else t_id + }) + + total_tables = len(matching_tables) + + send_stream_notification( + "start", + {"message": f"Start fetching {total_tables} price slots"}, + batch_id, 0, total_tables, total_tables, user_id, "price" + ) + + if total_tables == 0: + send_stream_notification("end", {"message": "No PriceSlot tables found"}, batch_id, 0, 0, 0, user_id, "price") + return + + headers_grist = {"Authorization": f"Bearer {GRIST_API_KEY}"} + + for current_idx, table_info in enumerate(matching_tables, start=1): + t_id = table_info["id"] + t_name = table_info["name"] + + meta_url = f"{GRIST_URL.rstrip('/')}/api/docs/{doc_price}/tables/{t_id}/columns" + resp_meta = request_with_retry("GET", meta_url, headers=headers_grist) + + header = [] + if resp_meta.status_code == 200: + columns = resp_meta.json().get("columns", []) + sorted_cols = sorted(columns, key=lambda c: col_to_index(c.get("id", "A"))) + header = [c.get("fields", {}).get("label", "") for c in sorted_cols] + + table_data = fetch_grist_table_data(doc_price, t_id) + + payload = [] + for row_obj in table_data: + row_id = row_obj["id"] + fields = row_obj["fields"] + + cells = [] + for col_idx, val in enumerate(fields): + cells.append({ + "value": str(val) if val else "", + "coord": { + "row": row_id, + "col": col_idx + 1 + } + }) + + payload.append({ + "row_index": row_id, + "cells": cells + }) + + chunk_data = [{ + "sheet": t_name, + "header": header, + "payload": payload + }] + + send_stream_notification("chunk", chunk_data, batch_id, current_idx, total_tables, total_tables, user_id, "price") + + send_stream_notification( + "end", + {"message": "All price slots sent successfully"}, + batch_id, total_tables, total_tables, total_tables, user_id, "price" + ) + + except Exception as e: + traceback.print_exc() + send_stream_notification("error", {"error_detail": str(e)}, batch_id, 0, 0, 0, user_id, "price") + +def handle_add_price(country: str, content: list, option: str = ""): config = COUNTRY_MAPPING.get(country) if not config: - print(f"[{SERVICE_NAME}] Country {country} config not found") - return + raise Exception (f"[{SERVICE_NAME}] Country {country} config not found") grist_docs = config.get("grist_doc_id", {}) doc_price = grist_docs.get("price") if not doc_price: - print(f"[{SERVICE_NAME}] Price doc not found for country {country}") - return + raise Exception (f"[{SERVICE_NAME}] Price doc not found for country {country}") + + table_id = option if option else "Price" records = [] for item in content: @@ -1706,11 +1890,10 @@ def handle_add_price(country: str, content: list): records.append(to_grist_record(cells)) if not records: - print(f"[{SERVICE_NAME}] No price records to add") - return + raise Exception (f"[{SERVICE_NAME}] No price records to add") headers = {"Authorization": f"Bearer {GRIST_API_KEY}", "Content-Type": "application/json"} - url = f"{GRIST_URL.rstrip('/')}/api/docs/{doc_price}/tables/Price/records" + url = f"{GRIST_URL.rstrip('/')}/api/docs/{doc_price}/tables/{table_id}/records" try: resp = request_with_retry( @@ -1724,10 +1907,10 @@ def handle_add_price(country: str, content: list): else: print(f"[{SERVICE_NAME}] Error adding price records: {resp.text}") except Exception as e: - print(f"[{SERVICE_NAME}] Request failed for add price: {e}") + raise Exception (f"[{SERVICE_NAME}] Request failed for add price: {e}") -def handle_update_price(country: str, content: list): +def handle_update_price(country: str, content: list, option: str = ""): """ Update price to Grist content: [{"row_index": 361, "cells": [{"value": "x", "coord": {...}}, ...]}, ...] @@ -1741,8 +1924,9 @@ def handle_update_price(country: str, content: list): doc_price = grist_docs.get("price") if not doc_price: - print(f"[{SERVICE_NAME}] Price doc not found for country {country}") - return + raise Exception (f"[{SERVICE_NAME}] Price doc not found for country {country}") + + table_id = option if option else "Price" records_to_update = [] @@ -1764,11 +1948,10 @@ def handle_update_price(country: str, content: list): }) if not records_to_update: - print(f"[{SERVICE_NAME}] No price records to update") - return + raise Exception (f"[{SERVICE_NAME}] No price records to update") headers = {"Authorization": f"Bearer {GRIST_API_KEY}", "Content-Type": "application/json"} - update_url = f"{GRIST_URL.rstrip('/')}/api/docs/{doc_price}/tables/Price/records" + update_url = f"{GRIST_URL.rstrip('/')}/api/docs/{doc_price}/tables/{table_id}/records" # Group by field keys (Grist PATCH requirement) grouped_updates = {} @@ -1791,33 +1974,32 @@ def handle_update_price(country: str, content: list): else: print(f"[{SERVICE_NAME}] Price update failed: {resp.text}") except Exception as e: - print(f"[{SERVICE_NAME}] Request failed for update price: {e}") + raise Exception (f"[{SERVICE_NAME}] Request failed for update price: {e}") -def handle_delete_price(country: str, content: list): +def handle_delete_price(country: str, content: list, option: str = ""): """ content: [{"target_id": 389}, {"target_id": 393}] """ config = COUNTRY_MAPPING.get(country) if not config: - print(f"[{SERVICE_NAME}] Country {country} config not found") - return + raise Exception (f"[{SERVICE_NAME}] Country {country} config not found") grist_docs = config.get("grist_doc_id", {}) doc_price = grist_docs.get("price") if not doc_price: - print(f"[{SERVICE_NAME}] Price doc not found for country {country}") - return + raise Exception (f"[{SERVICE_NAME}] Price doc not found for country {country}") + + table_id = option if option else "Price" target_ids = [int(item.get("target_id")) for item in content if item.get("target_id")] if not target_ids: - print(f"[{SERVICE_NAME}] Payload to delete price is empty") - return + raise Exception (f"[{SERVICE_NAME}] Payload to delete price is empty") headers = {"Authorization": f"Bearer {GRIST_API_KEY}", "Content-Type": "application/json"} - del_url = f"{GRIST_URL.rstrip('/')}/api/docs/{doc_price}/tables/Price/records/delete" + del_url = f"{GRIST_URL.rstrip('/')}/api/docs/{doc_price}/tables/{table_id}/records/delete" try: resp = request_with_retry( @@ -1828,7 +2010,7 @@ def handle_delete_price(country: str, content: list): ) print(f"[{SERVICE_NAME}] Deleted Price IDs: {target_ids} | Status: {resp.status_code}") except Exception as e: - print(f"[{SERVICE_NAME}] Delete price error: {e}") + raise Exception (f"[{SERVICE_NAME}] Delete price error: {e}") app = FastAPI() @@ -2131,8 +2313,7 @@ def get_column_letter(n): def sync_sheets_to_grist(country_key): if country_key not in COUNTRY_MAPPING: - print(f"Error: Country '{country_key}' not found.") - return + raise Exception (f"[{SERVICE_NAME}] Error: Country '{country_key}' not found.") config = COUNTRY_MAPPING[country_key] spreadsheet_id = config["spreadsheet_id"] @@ -2144,17 +2325,17 @@ def sync_sheets_to_grist(country_key): for sheet_key, sheet_name in sheets.items(): if not sheet_name: - continue + raise Exception (f"[{SERVICE_NAME}] Error: Sheet: '{sheet_name}' not found.") doc_id = grist_doc_ids.get(sheet_key) if not doc_id: - continue + raise Exception (f"[{SERVICE_NAME}] Error: DocumentId: '{doc_id}' not found.") try: worksheet = spreadsheet.worksheet(sheet_name) all_values = worksheet.get_all_values() if not all_values: - continue + raise header = all_values[0] data_rows = all_values[1:] @@ -2163,11 +2344,34 @@ def sync_sheets_to_grist(country_key): process_new_layout_sheet(doc_id, header, data_rows) elif sheet_key == "name-desc-v2": upload_to_grist_self_hosted(doc_id, "name-desc-v2", header, data_rows) + # elif sheet_key == "price": + # upload_to_grist_self_hosted(doc_id, "price", header, data_rows) elif sheet_key == "price": upload_to_grist_self_hosted(doc_id, "price", header, data_rows) + all_worksheets = spreadsheet.worksheets() + + price_slot_pattern = re.compile(r"^PriceSlot[1-9]\d*$") + + for ws in all_worksheets: + ws_name = ws.title + + if price_slot_pattern.match(ws_name): + try: + slot_values = ws.get_all_values() + if not slot_values: + raise + + slot_header = slot_values[0] + slot_data = slot_values[1:] + + upload_to_grist_self_hosted(doc_id, ws_name, slot_header, slot_data) + + except Exception as slot_err: + raise Exception (f"[{SERVICE_NAME}] Error processing dynamic sheet {ws_name}: {slot_err}") + except Exception as e: - print(f"[{SERVICE_NAME}] Error processing sheet {sheet_name}: {e}") + raise Exception (f"[{SERVICE_NAME}] Error processing sheet {sheet_name}: {e}") def process_new_layout_sheet(doc_id, header, data_rows): table_groups = {}