[sheet-service] Management price & price slot, set config for any country

This commit is contained in:
Ittipat Lusuk 2026-06-17 11:32:41 +07:00
parent 6fc7cb5265
commit 7cb95aee83

584
main.py
View file

@ -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 = {}