diff --git a/src/__tests__/fixtures/orders/real/dd-01.html b/src/__tests__/fixtures/orders/real/dd-01.html new file mode 100644 index 0000000..fbfa087 --- /dev/null +++ b/src/__tests__/fixtures/orders/real/dd-01.html @@ -0,0 +1,549 @@ + + + + DoorDash + + + + + + + + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
DOORDASH
+ + + + + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + + + +
+ + + + + + + +
+ + + + + + + +
+ + + + + + + +
+ + + + + + + +
+ + + + + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + + +
+ + \ No newline at end of file diff --git a/src/__tests__/fixtures/orders/real/dd-08.html b/src/__tests__/fixtures/orders/real/dd-08.html new file mode 100644 index 0000000..54f5402 --- /dev/null +++ b/src/__tests__/fixtures/orders/real/dd-08.html @@ -0,0 +1,817 @@ + + + + + + + + + + + DoorDash + + + + + + + + + + + +
+
+  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏ +
+ + + + + + +
+ + + + + + + + + + + + + + + + +
+
+ + \ No newline at end of file diff --git a/src/__tests__/fixtures/orders/real/dd-10.html b/src/__tests__/fixtures/orders/real/dd-10.html new file mode 100644 index 0000000..fd6f1a4 --- /dev/null +++ b/src/__tests__/fixtures/orders/real/dd-10.html @@ -0,0 +1,592 @@ + + + + + + + + + + + DoorDash + + + + + + + + + + + +
+
+  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏  ­͏ +
+ + + + + + +
+ + + + + + + + + + + + + + + + +
+
+ + \ No newline at end of file diff --git a/src/__tests__/fixtures/orders/real/dd-27.html b/src/__tests__/fixtures/orders/real/dd-27.html new file mode 100644 index 0000000..c7247b6 --- /dev/null +++ b/src/__tests__/fixtures/orders/real/dd-27.html @@ -0,0 +1,554 @@ + + + + DoorDash + + + + + + + + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
DOORDASH
+ + + + + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ + + + + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + + + +
+ + + + + + + +
+ + + + + + + +
+ + + + + + + +
+ + + + + + + +
+ + + + + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + + +
+ + \ No newline at end of file diff --git a/src/__tests__/fixtures/orders/real/dd-meta.json b/src/__tests__/fixtures/orders/real/dd-meta.json new file mode 100644 index 0000000..a41e0a6 --- /dev/null +++ b/src/__tests__/fixtures/orders/real/dd-meta.json @@ -0,0 +1,254 @@ +[ + { + "id": "19f89664ea7b3aeb", + "date": "Date: Wed, 22 Jul 2026 10:36:50 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Homey Meals", + "len": 38394 + }, + { + "id": "19f68fd09cd2c3f1", + "date": "Date: Thu, 16 Jul 2026 03:34:00 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Mad Mex", + "len": 37448 + }, + { + "id": "19f656a298f42858", + "date": "Date: Wed, 15 Jul 2026 10:54:42 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Carl's Jr.", + "len": 37970 + }, + { + "id": "19f605de9ab82216", + "date": "Date: Tue, 14 Jul 2026 11:23:14 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Delhi Nights - Sweets & Indian\r\n Cuisine", + "len": 38707 + }, + { + "id": "19f2b6390a95b665", + "date": "Date: Sat, 04 Jul 2026 04:29:32 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from TEG KEBABS & BIRYANI", + "len": 38005 + }, + { + "id": "19f2256c51714b9c", + "date": "Date: Thu, 02 Jul 2026 10:18:58 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Sri Dwaraka", + "len": 37817 + }, + { + "id": "19f1d0b003834626", + "date": "Date: Wed, 01 Jul 2026 09:38:07 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Taco Bell", + "len": 38019 + }, + { + "id": "19f1c301dda7332c", + "date": "Date: Wed, 01 Jul 2026 05:39:01 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Woolworths", + "len": 38753 + }, + { + "id": "19f0c5624707ae34", + "date": "Date: Sun, 28 Jun 2026 03:46:39 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from ALDI", + "len": 68206 + }, + { + "id": "19f0c0d0c33bf5c9", + "date": "Date: Sun, 28 Jun 2026 02:26:49 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from ALDI", + "len": 47419 + }, + { + "id": "19eed89eee79b624", + "date": "Date: Mon, 22 Jun 2026 04:14:59 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Woolworths", + "len": 48653 + }, + { + "id": "19eed498e3fb7fce", + "date": "Date: Mon, 22 Jun 2026 03:04:39 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Woolworths", + "len": 44104 + }, + { + "id": "19eed38ca77047a1", + "date": "Date: Mon, 22 Jun 2026 02:46:21 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Oporto", + "len": 38675 + }, + { + "id": "19ee9b9b1b7073c9", + "date": "Date: Sun, 21 Jun 2026 10:28:39 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Chilli India", + "len": 37871 + }, + { + "id": "19e95be74e216594", + "date": "Date: Fri, 05 Jun 2026 03:05:46 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Chilli India", + "len": 37817 + }, + { + "id": "19e854cdbf552dcf", + "date": "Date: Mon, 01 Jun 2026 22:27:45 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from TOMKINS BAKERY", + "len": 37741 + }, + { + "id": "19e8020eb0027109", + "date": "Date: Sun, 31 May 2026 22:21:39 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Tonmax Bakery", + "len": 37439 + }, + { + "id": "19e16b64b3f6dbad", + "date": "Date: Mon, 11 May 2026 11:05:04 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Woolworths", + "len": 39750 + }, + { + "id": "19e16b1a2d9526f8", + "date": "Date: Mon, 11 May 2026 11:00:00 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Burger Road", + "len": 38230 + }, + { + "id": "19e11390ea456222", + "date": "Date: Sun, 10 May 2026 09:30:11 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Chilli India", + "len": 37847 + }, + { + "id": "19cdfa1d25f194a8", + "date": "Date: Thu, 12 Mar 2026 01:20:48 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Kesari Indian Kitchen", + "len": 37922 + }, + { + "id": "19cc62db0460b8a0", + "date": "Date: Sat, 07 Mar 2026 02:43:28 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Chilli India", + "len": 37884 + }, + { + "id": "19ca1f0fa493f0ca", + "date": "Date: Sat, 28 Feb 2026 01:50:49 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Chilli India", + "len": 37862 + }, + { + "id": "19c9c18ad4bf22e1", + "date": "Date: Thu, 26 Feb 2026 22:36:27 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Hungry Jacks", + "len": 37596 + }, + { + "id": "19c8ef02c0a0f590", + "date": "Date: Tue, 24 Feb 2026 09:17:09 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Schnitz", + "len": 38107 + }, + { + "id": "19c6e7cb5565afd5", + "date": "Date: Wed, 18 Feb 2026 02:03:12 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Chilli India", + "len": 37418 + }, + { + "id": "19c4c4b1a0efa1e0", + "date": "Date: Wed, 11 Feb 2026 10:41:56 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from lahori fish n tikka", + "len": 37166 + }, + { + "id": "19c4013c5a7fac93", + "date": "Date: Mon, 09 Feb 2026 01:46:02 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Subway", + "len": 39212 + }, + { + "id": "19c30b95bf183a2b", + "date": "Date: Fri, 06 Feb 2026 02:12:59 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Souvlaki GR", + "len": 37776 + }, + { + "id": "19c30b773199c2fb", + "date": "Date: Fri, 06 Feb 2026 02:10:54 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Souvlaki GR", + "len": 37309 + }, + { + "id": "19c27f770fae04f2", + "date": "Date: Wed, 04 Feb 2026 09:24:13 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Chilli India", + "len": 37440 + }, + { + "id": "19c21316b26e066f", + "date": "Date: Tue, 03 Feb 2026 01:50:11 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Punjabi bai sweets & Indian\r\n cuisine", + "len": 39289 + }, + { + "id": "19bf2e0fd25facda", + "date": "Date: Sun, 25 Jan 2026 01:59:48 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Chilli India", + "len": 38432 + }, + { + "id": "19bb18096ebd9bf5", + "date": "Date: Mon, 12 Jan 2026 09:19:12 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Woolworths", + "len": 42464 + }, + { + "id": "19bac527ef5fb1d4", + "date": "Date: Sun, 11 Jan 2026 09:10:45 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Chilli India", + "len": 37833 + }, + { + "id": "19baad6dd7f80a9f", + "date": "Date: Sun, 11 Jan 2026 02:16:04 +0000 (UTC)", + "from": "From: DoorDash Order ", + "subject": "Subject: Order Confirmation for Siddharth from Chilli India", + "len": 37334 + } +] \ No newline at end of file diff --git a/src/__tests__/fixtures/orders/real/ue-00.html b/src/__tests__/fixtures/orders/real/ue-00.html new file mode 100644 index 0000000..fd9d537 --- /dev/null +++ b/src/__tests__/fixtures/orders/real/ue-00.html @@ -0,0 +1,254 @@ + +
\ No newline at end of file diff --git a/src/__tests__/fixtures/orders/real/ue-04.html b/src/__tests__/fixtures/orders/real/ue-04.html new file mode 100644 index 0000000..19b6722 --- /dev/null +++ b/src/__tests__/fixtures/orders/real/ue-04.html @@ -0,0 +1,274 @@ + +
\ No newline at end of file diff --git a/src/__tests__/fixtures/orders/real/ue-05.html b/src/__tests__/fixtures/orders/real/ue-05.html new file mode 100644 index 0000000..3b2f1ff --- /dev/null +++ b/src/__tests__/fixtures/orders/real/ue-05.html @@ -0,0 +1,254 @@ + +
\ No newline at end of file diff --git a/src/__tests__/fixtures/orders/real/ue-09.html b/src/__tests__/fixtures/orders/real/ue-09.html new file mode 100644 index 0000000..26082a4 --- /dev/null +++ b/src/__tests__/fixtures/orders/real/ue-09.html @@ -0,0 +1,274 @@ + +
\ No newline at end of file diff --git a/src/__tests__/fixtures/orders/real/ue-26.html b/src/__tests__/fixtures/orders/real/ue-26.html new file mode 100644 index 0000000..f863aea --- /dev/null +++ b/src/__tests__/fixtures/orders/real/ue-26.html @@ -0,0 +1,254 @@ + +
\ No newline at end of file diff --git a/src/__tests__/fixtures/orders/real/ue-meta.json b/src/__tests__/fixtures/orders/real/ue-meta.json new file mode 100644 index 0000000..1ee95da --- /dev/null +++ b/src/__tests__/fixtures/orders/real/ue-meta.json @@ -0,0 +1,205 @@ +[ + { + "i": 0, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAkeckxXAAAA", + "date": "2026-07-22T03:41:19Z", + "subject": "Your Wednesday afternoon order with Uber Eats", + "len": 57778 + }, + { + "i": 1, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAkd32TzAAAA", + "date": "2026-07-21T04:25:09Z", + "subject": "Your Tuesday afternoon order with Uber Eats", + "len": 61532 + }, + { + "i": 2, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAkd32TsAAAA", + "date": "2026-07-20T23:17:05Z", + "subject": "Your Tuesday morning order with Uber Eats", + "len": 59110 + }, + { + "i": 3, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAkdK0voAAAA", + "date": "2026-07-20T04:23:40Z", + "subject": "Your Monday afternoon order with Uber Eats", + "len": 66211 + }, + { + "i": 4, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAkR0aigAAAA", + "date": "2026-07-07T15:31:27Z", + "subject": "[Family] Your Tuesday evening order with Uber Eats", + "len": 80279 + }, + { + "i": 5, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAkR0aicAAAA", + "date": "2026-07-07T08:58:43Z", + "subject": "Your Tuesday evening order with Uber Eats", + "len": 73592 + }, + { + "i": 6, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAkR0aibAAAA", + "date": "2026-07-07T08:50:06Z", + "subject": "[Family] Your Tuesday afternoon order with Uber Eats", + "len": 78782 + }, + { + "i": 7, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAkR0aiaAAAA", + "date": "2026-07-07T08:46:17Z", + "subject": "Your Tuesday evening order with Uber Eats", + "len": 65886 + }, + { + "i": 8, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAkR0ah6AAAA", + "date": "2026-07-05T15:51:11Z", + "subject": "[Family] Your Sunday evening order with Uber Eats", + "len": 82383 + }, + { + "i": 9, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAkR0ah5AAAA", + "date": "2026-07-05T15:33:01Z", + "subject": "[Family] Your Sunday evening order with Uber Eats", + "len": 88958 + }, + { + "i": 10, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAkR0ahTAAAA", + "date": "2026-07-03T10:04:15Z", + "subject": "Your Friday evening order with Uber Eats", + "len": 61485 + }, + { + "i": 11, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAkR0ahFAAAA", + "date": "2026-07-02T22:57:18Z", + "subject": "Your Friday morning order with Uber Eats", + "len": 60751 + }, + { + "i": 12, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAkOIIcBAAAA", + "date": "2026-06-27T04:21:08Z", + "subject": "Your Saturday afternoon order with Uber Eats", + "len": 60720 + }, + { + "i": 13, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAkH8zVTAAAA", + "date": "2026-06-22T10:19:49Z", + "subject": "Your Monday evening order with Uber Eats", + "len": 58303 + }, + { + "i": 14, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAkH8zUwAAAA", + "date": "2026-06-20T12:28:26Z", + "subject": "Your Saturday afternoon order with Uber Eats", + "len": 60856 + }, + { + "i": 15, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAkH8zURAAAA", + "date": "2026-06-18T18:43:06Z", + "subject": "Your Thursday evening order with Uber Eats", + "len": 57881 + }, + { + "i": 16, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAkGJuBoAAAA", + "date": "2026-06-15T19:32:17Z", + "subject": "Your Monday evening order with Uber Eats", + "len": 60838 + }, + { + "i": 17, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAj9_fg8AAAA", + "date": "2026-06-02T11:27:03Z", + "subject": "Your Tuesday evening order with Uber Eats", + "len": 55147 + }, + { + "i": 18, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAj7_L67AAAA", + "date": "2026-06-02T03:11:33Z", + "subject": "Your Tuesday afternoon order with Uber Eats", + "len": 60716 + }, + { + "i": 19, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAj66TeuAAAA", + "date": "2026-05-29T02:47:06Z", + "subject": "Your Friday afternoon order with Uber Eats", + "len": 55127 + }, + { + "i": 20, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAj19QCWNwAAAA==", + "date": "2026-05-23T03:00:14Z", + "subject": "Your Saturday afternoon order with Uber Eats", + "len": 65562 + }, + { + "i": 21, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAjzoSJtAAAA", + "date": "2026-05-18T10:50:25Z", + "subject": "Your Monday evening order with Uber Eats", + "len": 68144 + }, + { + "i": 22, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAjy8e6sAAAA", + "date": "2026-05-17T03:13:05Z", + "subject": "Your Sunday afternoon order with Uber Eats", + "len": 62439 + }, + { + "i": 23, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAjw8ZyAAAAA", + "date": "2026-05-15T09:45:15Z", + "subject": "Your Friday evening order with Uber Eats", + "len": 55113 + }, + { + "i": 24, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAjw8ZxwAAAA", + "date": "2026-05-14T15:02:24Z", + "subject": "Your Thursday afternoon order with Uber Eats", + "len": 58231 + }, + { + "i": 25, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAjw8ZxtAAAA", + "date": "2026-05-14T11:24:43Z", + "subject": "Your Thursday evening order with Uber Eats", + "len": 60745 + }, + { + "i": 26, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAjV0eF4AAAA", + "date": "2026-04-08T05:54:03Z", + "subject": "Your Tuesday evening order with Uber Eats", + "len": 68338 + }, + { + "i": 27, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAjV0eFyAAAA", + "date": "2026-04-07T22:40:52Z", + "subject": "Your Tuesday afternoon order with Uber Eats", + "len": 52600 + }, + { + "i": 28, + "id": "AQMkADAwATM0MDAAMS1hYzQyLTVjZDktMDACLTAwCgBGAAADS6kDcxBxKkKwbCK1wU1QlwcA7Ex_hlaTTU0AgHQ16optl_AAAAIBDAAAAOxMfoZWk01NAIB0NeqKbZfgAAjV0eFcAAAA", + "date": "2026-04-06T05:23:40Z", + "subject": "Your Sunday evening order with Uber Eats", + "len": 59440 + } +] \ No newline at end of file diff --git a/src/lib/order-parse.ts b/src/lib/order-parse.ts new file mode 100644 index 0000000..775c983 --- /dev/null +++ b/src/lib/order-parse.ts @@ -0,0 +1,433 @@ +/** + * Order receipt parsing, written against REAL captured emails. + * + * History: the first version of this parser was written against synthetic + * fixtures that were shaped to match the code rather than the mail. It invented + * a layout that DoorDash does not + * send, derived order_reference from Math.random(), and read the order date + * from a `Date: YYYY-MM-DD` string that appears in no real message. All of it + * passed its tests. This version is built from 36 real DoorDash and 29 real + * Uber Eats receipts; see docs in memory-bank/order-ingestion-implementation.md. + * + * Governing rule: parse or throw. Never fabricate a value that the mail did not + * state (I9). A caller that gets a ParsedOrder back can trust every field in it. + */ + +export interface LineItem { + qty: number; + description: string; + amount: number; + options?: string[]; +} + +export interface PaymentBreakdown { + credits_amount: number | null; + card_amount: number | null; + card_last4: string | null; + /** true when the mail states a payment method it does not fully disaggregate. */ + ambiguous: boolean; +} + +export interface OrderTotals { + subtotal: number | null; + taxes: number | null; + delivery_fee: number | null; + service_fee: number | null; + tip: number | null; + discounts: number | null; + total_charged: number; +} + +export interface ParsedOrder { + order_reference: string; + platform: "doordash" | "ubereats" | "uber"; + merchant_name: string; + order_datetime: string; + currency: string; + payment: PaymentBreakdown; + totals: OrderTotals; + line_items: LineItem[]; + is_family: boolean; + flags: string[]; +} + +/** Everything the parser needs that lives on the message, not in the body. */ +export interface MessageMeta { + /** Provider message id. The only stable per-mail identity DoorDash offers. */ + messageId: string; + subject: string; + /** ISO 8601. The authoritative order date — the body carries no reliable one. */ + receivedAt: string; + sender?: string; +} + +export class OrderParseError extends Error { + constructor(message: string, readonly messageId?: string) { + super(message); + this.name = "OrderParseError"; + } +} + +const stripTags = (s: string) => s.replace(/<[^>]+>/g, " "); + +const decodeEntities = (s: string) => + s + .replace(/ /gi, " ") + .replace(/&/gi, "&") + .replace(/'|'/gi, "'") + .replace(/"/gi, '"') + .replace(/$/g, "$") + .replace(/…/gi, "…"); + +const collapse = (s: string) => s.replace(/\s+/g, " ").trim(); + +const money = (raw: string): number => Math.abs(parseFloat(raw.replace(/[$,]/g, ""))); + +/** + * DoorDash renders each total as its own nested table: + * + * Reading the pair structurally is what stops the label/value rebinding that + * flattening causes (I9) — flattened, "Discounts -$24.09 Total Charged $14.64" + * invites a regex to bind the wrong number to the wrong label. + */ +function tdPairValue(html: string, label: string): number | null { + const re = new RegExp( + `]*>\\s*${label}\\s*\\s*]*>\\s*(-?\\s*\\$?[\\d,]+\\.\\d{2})\\s*`, + "i" + ); + const m = html.match(re); + return m ? money(m[1]) : null; +} + +function parseDoorDashLineItems(html: string): LineItem[] { + // + const re = + /]*width="10%"[^>]*>\s*(\d+)x\s*<\/td>\s*]*width="75%"[^>]*>([\s\S]*?)<\/td>\s*]*width="15%"[^>]*>\s*\$?([\d,]+\.\d{2})\s*<\/td>/gi; + const items: LineItem[] = []; + for (const m of html.matchAll(re)) { + const parts = decodeEntities(stripTags(m[2])) + .split("•") + .map((p) => collapse(p)) + .filter(Boolean); + if (!parts.length) continue; + items.push({ + qty: parseInt(m[1], 10), + description: parts[0], + amount: money(m[3]), + options: parts.slice(1), + }); + } + return items; +} + +/** Reads "
Label$X
Subtotal $22.101xName (Cat)
• Opt
$22.10