# ETA Service ## Why? Currently, ETA logic is spread across multiple services and ETAs themselves are primarily stored as a single `min - max` range that acts as a 'global ETA' for a shop. Some services treat ETAs differently, performing calculations, padding minutes etc on the baseline ETA values. Other services like Search Service store weekly ETA schedules as well as additional schedules related to the V2 ETA experiment which necessitates logic in Search Service to determine which ETA should be used. Given how ETAs currently exist, making changes to underlying ETA logic often requires updates across multiple services which is less than desirable. Having logic spread across services also makes it tricky to fully grasp what ETAs come from where, how they're calculated, how they're updated etc. Having them persisted primarily as a single char value thats parsed into a numeric range also doesn't allos us to accuratly represent the dynamic nature of an ETA that changes throughout a day. An ETA service that acts as the source of truth for ETAs and their related logic aligns with the Slice 'north star' architecture goals. This will be a domain specific microservice and aid in decomposing our existing monoliths while centralizing ETA logic in one location. This will allow us to iterate faster and more confidently on future ETA changes while reducing the burden of maintaining ETA related logic. ## Phasing This service can be broken in to two logical phases initially: ### Pre-order ETAS This phase would be focused on building out the functionality to allow shops/AMs/Admins to create and read ETAs and ETA schedules. Anything that creates or reads an ETA for a shop would fall into this phase. This phase would include the work needed to build out the new service and the requisite functionality as well as transitioning any calls to create or read pre-order ETAs to the new service. Key considerations for this phase include: - Customers see pre-order etas on menu/shop pages which are currently returned from Core - Customers see pre-order etas in search results which come from Search Service and require having a full eta schedule - Shops/owners/AMs/Support see pre-order etas in places where they can manage shop details - DoorDash and third-party delivery integrations which might have different ETAs See [Where Do Etas Come From](https://docs.google.com/document/d/1wsXSgfL3HuTG4OH2WKZFwDoZignlYff6H6He8O-sq7w/edit) ### Post-order ETAS This phase will include the work to create and read ETA info for orders on an individual order basis. We'll have to think more on this phase as one consideration is that order ETAs are pretty coupled with the order confirmation process. Key considerations for this phase include: - Orders are confirmed in various locations (email, admin, slicos, register) and all of these confirmations currently flow to Admin with some being message based (_I think_) - Updating an order's ETA often triggers a communication so we'll have to be cognizent of the fact that updating an ETA through our system needs to trigger the appropriate communication downstream - DoorDash and third-party delivery integrations which might have different ETAs ## Core Functionality Of An ETA Service ### Pre-Order ETAs #### Create/Update ETAs for a shop - An ETA service should be capable of creating/updating a shops default ETA - Creating/updating a full ETA schedule for a shop - Creating/updating an ETA for a specific time period for a shop #### Read ETAs for a shop - An ETA service should be capable of returning the current ETA for a shop - An ETA for a future time for a shop - An ETA schedule for a shop #### Publish ETA updates for a shop - An ETA service should be capable of publishing ETA updates, likely a full ETA schedule, so that services that rely on ETAs (like Search Service) can learn about ETA updates as they happen ### Post-Order ETAs #### Create/Update ETAs for an order - An ETA service should be capable of creating an ETA for an order or updating an order's ETA #### Read ETAs for an order - An ETA service should be capable of returning an order's ETA - Also return appropriate alternate confirmation eta options? - Order ETAs should be capable of being requested in multiple formats - ETA as a minute range (`30 - 45 min`) - ETA as a timestamp rang (`5:30 - 5:45`) #### Publishing ETA updates for an order - An ETA service should be capable of publishing updates for an order's ETA so that downstream services can trigger appropriate notifications ## Consderations ### Data Model - We should be storing ETA schedules - Currently ETAs are stored in the following ways - As single `varchar(255)` field on the `shop_master` table (`delivery_estimate`/`pickup_estimate`) in the format `x - y min` - An array of `ints` in Elasticsearch - Each array position represents a temporal unit with the value stored in the position being the ETA for that moment in time - We currently have `168` and `672` length ETA schedule arrays stored in Elasticsearch - We use two arrays for delivery ETAs, one for min and one for max ETA values - A weekly ETA schedule should be persisted - An order's `created_at` and `confirmed_at` are values we'll be interested in as the allow us to calculate ETA timestamps - Do we care about shop timezone? Can we get by with returning timestamps in UTC always? #### Ways we can represent an ETA schedule ##### Array/Matrix Based Approaches * These approaches to modeling a schedule have the advantage of fast look up - You can calculate the current day/hour offset and access a eta by index position. * These structures come at the cost of readability. Its not easy on human readability and leaves open more room for error when building out these long arrays/matrices. * Persisting this style of schedule would also require a noSQL datastore that supports array based datatypes. * Array based approaches inherently deal with overlaping schedule units - you can't index two etas in the same array position * Most datastores don't support querying documents and accessing array values by index - Single arrays for min and max ``` { "shop_id": 1, "min_delivery": [ 30, 30, 30, 30.... ], "max_delivery": [ 45, 45, 45, 45.... ] } current_max = max[hour_of_week_index] current_min = min[hour_of_week_index] ``` - Nested arrays for both ``` { "shop_id": 1 "delivery_eta_schedule": [[30, 45], [30, 45], [30, 45], [30, 45].... ] } current_max = schedule[hour_of_week_index][1] current_min = schedule[hour_of_week_index][0] ``` - Matrix ``` { "shop_id": 1, "delivery_eta_schedule": [ [[30, 45], [30, 45], [30, 45], [30, 45] ....], [[30, 45], [30, 45], [30, 45], [30, 45] ....], [[30, 45], [30, 45], [30, 45], [30, 45] ....], [[30, 45], [30, 45], [30, 45], [30, 45] ....], [[30, 45], [30, 45], [30, 45], [30, 45] ....], [[30, 45], [30, 45], [30, 45], [30, 45] ....], [[30, 45], [30, 45], [30, 45], [30, 45] ....], ] } current_max = schedule[day_of_week_index][hour_of_day][1] current_min = schedule[day_of_week_index][hour_of_day][0] ``` ##### Other Data Structures Using a more composite data type would result in a hit on look up speed but will come with the advantage of readability. Looking at any of these structures/persistance strategies makes it relatively easy to read. - Time Ranges nosql - Something like this could be stored pretty much as-is in a datastore like Amazon DocumentDB (essentially MongoDB) which supports BSON structures - This also builds in flexability since we've seen ETAs and how they're stored and accessed evolve quite a bit in just the time we've worked on them. Having the flexability to easily add additional etas, timestamp specific etas, experimentation schedules, special flags etc makes iterating with this type of datastore easier and faster - TODO: Look into documentDB read performance - TODO: Look into plucking out active nested docs from `delivery_etas` without returning the entire root document - Looks like MongoDB supports this with projection ``` # one doc per shop { "shop_id": 1 "default_eta_min": 30, "default_eta_max": 45, "delivery_etas": [ { "day": "Tuesday", "from": 1000, "to": 1300, "eta_max": 55, "eta_min": 40, }, { "day": "Tuesday" "from": 1300, "to": 2000, "eta_max": 75, "eta_min": 60, } ] } # one eta per doc { "shop_id": 1, "order_type": "delivery", "day": "Tuesday", "from": 1000, "to": 1300, "eta_max": 55, "eta_min": 40, } ``` - Time Ranges relational db - We use similar storage methods for shop opening/closing schedules - Relational dbs are something we're familiar working with - The lookup speed is fast and queries are succinctly expressed in SQL - Harder and more time consuming to change the represtnation or add fields etc - Experience with v2 etas and data experimenation should have us consider if relational db would make it hard to develope future experimentation quickly | id | shop_id | from | to | eta_min | eta_max | day_of_week | | ---- | ------- | ----- | ----- | ------- |:-------:| ----------- | | 1 | 1 | 12:00 | 15:00 | 30 | 45 | Tuesday | | 2 | 1 | 15:01 | 20:00 | 40 | 55 | Tuesday | ### Experimentation - Implementing experimenation with the v2 ETAs was non-trivial given how ETAs are currently stored and used - Since we'll be supporting experimenation out of the gate, it should be something we consider up front rather than as an afterthought ``` { "shop_id": 1 "default_eta_min": 30, "default_eta_max": 45, "delivery_etas": [ { "day": "Tuesday", "from": 1000, "to": 1300, "eta_max": 55, "eta_min": 40, } ], "delivery_etas_experiment_v3": [ { "day": "Tuesday" "from": 1300, "to": 2000, "eta_max": 75, "eta_min": 60, } ] } ``` - Can we support user based ETAs? ### Other - Timezones, they suck. `Tuesday 10:00` would be in UTC so we'd be looking to accept data in UTC on converting it to UTC - Shop sets eta `from` as ` Wednesday, February 23, 2022 3:00:00 PM GMT-07:00` we would create an item with `from` as `Wednesday, February 23, 2022 10:00:00 PM GMT-00:00` - How can we handle/support/encorporate user specific info into ETAs? - Consider ETAs for an order accounting for drive time between users lat/long and the shop - If we're running experimenation an eta service might have multiple experimental versions and the logic to select the appropriate one. Search Service should probably be constrained to only ever having two eta versions, a default and a experiment version ## Definitions #### Pre-order ETA #### Post-order ETA #### Default ETA #### Max/Min ETA <br /> ## Datastore Investigation Shortcut ticket - https://app.shortcut.com/slicelife/story/297351/spike-investigate-datastores-for-eta-service #### Usecases to consider - A shop sets an eta from 5-8pm of 40-55 mins. The shop later goes and updates their eta schedule, setting 7-8pm to 50-65 minutes. 5-6:59pm should still have the 40-55 min eta set. - This usecase makes a case for standardizing our ranges by breaking a day into 5 or 10 minute intervals. Setting 5-8pm would actually set every 10 minute interval between 5-8pm with the provided eta - This usecase could also arise with data science provided etas with the shop setting an eta from 5-8 and data science providing an alternative eta from 6-7. The 6-7 eta intervals could store both the shop set eta and the data science eta ### MongoDB Played around with queries in https://mongoplayground.net/. Used the below sample data as the configuration. As mentioned above, grabbing an array element by index in MongoDB isn't straightforward, I wasn't able to get a working query in the end. This made querying the array/matrix schedules a non starter. The [composite schedule](https://hackmd.io/Ppk7_atKS-S-qBdMO7L_5g?view#Composite-Eta-Ranges-tofrom-in-minutes-of-day) was much easier to work with. The `delivery_etas` field is an array of eta objects. Each eta object contains a `day` of the week, an `eta_min`/`eta_max` and a `to`/`from` range, within which this eta would apply. I updated the `to` and `from` fields to floats, displaying the time in minutes of the day ranging from 0 (00:00) to 1440 (23:59). This allowed me to make use of MongoDBs [`$elemMatch` operator](https://www.mongodb.com/docs/manual/reference/operator/query/elemMatch/) combined with less than (`$lt`) and greater than (`$gt`) queries. This approach produced the desired response. See query and response below. #### Mongodb Query with `$elemMatch` ##### Ranges of time in minutes of day ``` # MongoDB query # This query using $elemMatch on it’s own only returns this first element that matches db.collection.find({ shop_id: 101 }, { "default_eta_min": 1, "default_eta_max": 1, "delivery_etas": { $elemMatch: { day: "Monday", from: { $lt: 1 }, to: { $gt: 119 } } } }) ``` ``` # response [ { "_id": ObjectId("5a934e000102030405000000"), "delivery_etas": [ { "day": "Monday", "eta_max": 55, "eta_min": 40, "from": 0, "to": 120 } ] } ] ``` ##### Ranges of time using `ISODate` timestamps ``` # MongoDB query with `ISODate` timestamps # This query using $elemMatch on it’s own only returns this first element that matches db.collection.find({ shop_id: 101 }, { "default_eta_min": 1, "default_eta_max": 1, "delivery_etas": { $elemMatch: { day: "Monday", from: { $lt: ISODate("2022-06-06T01:00:00Z") }, to: { $gt: ISODate("2022-06-06T01:59:00Z") } } } }) ``` ``` # response [ { "_id": ObjectId("5a934e000102030405000001"), "delivery_etas": [ { "day": "Monday", "eta_max": 55, "eta_min": 40, "from": ISODate("2022-06-06T00:00:00Z"), "to": ISODate("2022-06-06T02:00:00Z") } ] } ] ``` **Important Note** - `$elemMatch` on it's own only returns this first element that matches. TODO: I attempted to build queries using dot notation instead of `$elemMatch`, i.e (`delivery_etas.day`). However, I was not able to get the `$lt`/`$gt` queries to work alongside it. Need to figure out if this is viable #### Mongodb Query with `$project` This query is able to return multiple items from a nested array We likely take a performance hit using this. ```json= db.collection.aggregate([ { "$match": { "shop_id": 101 }, }, { "$project": { "shop_id": 1, "default_eta_min": 1, "default_eta_max": 1, "delivery_eta_schedule": { "$filter": { "input": "$delivery_etas", "as": "delivery_etas", "cond": { "$and": [ { "$eq": [ "$$delivery_etas.day", "Monday" ] }, { "$lte": [ "$$delivery_etas.from", 65 ] }, { "$gte": [ "$$delivery_etas.to", 65 ] } ] } } } } } ]) ``` ```json= # Response [ { "_id": ObjectId("5a934e000102030405000000"), "default_eta_max": 40, "default_eta_min": 55, "delivery_eta_schedule": [ { "day": "Monday", "eta_max": 55, "eta_min": 40, "from": 0, "source": "shop_set", "to": 120 }, { "day": "Monday", "eta_max": 65, "eta_min": 50, "from": 60, "source": "data_science_v2", "to": 120 } ], "shop_id": 101 } ] ``` #### Mongodb query using `$and` for invidual eta records With this approach where each `x` minute interval has its own document rather than being in a nested array, we could define an arbitrary `day` called `Default` to store default values and update this query to query the day we're after or the day `Default` so one call is used to return ```json= db.collection.find({ "$and": [ { shop_id: 101 }, { day: "Monday" }, { from: { "$lt": 60 } }, { to: { "$gt": 60 } } ] }) ``` ```json= # Results [ { "_id": ObjectId("5a934e000102030405000000"), "day": "Monday", "eta_max_delivery": 55, "eta_max_pickup": 55, "eta_min_": 40, "eta_min_delivery": 40, "from": 0, "shop_id": 101, "to": 120 } ] ``` ### DynamoDB [Core Components of DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html) #### Setting DynamoDB up locally to test 1. `docker pull amazon/dynamodb-local` 2. `docker run -p 8000:8000 amazon/dynamodb-local` 3. Download [DynamoDB Workbench](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/workbench.settingup.html) 4. Finish setup in Workbench Dynamo appears to require that we re-think our data model. The primary key required can be either a `partition key` or a composite key of `partition key` and a `sort key` but these need to be unique identifiers. When modeling the eta schedule the ideal primary key would be a composite key of `shop_id`, `day`, `to` (shop, day of the week, minute of the day), however, this is three attributes rather than the allowed two. `shop_id` and `day` could be used as a composite key with each item including that day's schedule In the following examples, the number keys ("0", "15" etc) represent the minute of the day that the eta starts until it hits the next number key, essentially creating eta ranges of 15-minutes. One disadvantage of this `shop_id` + `day` composite key is that the default eta values would either need to be set on a per-day basis or using a `"day": "default"` item, however, this would allow shops to set default etas per-day rather than globally and we load a small sized object into memory when accessing a shop's schedule since we're only pulling out a single day. ```json= { "shop_id": 101, "day": "Monday", "delivery_schedule": { "default": { "eta_max": 50, "eta_min": 35 }, "0": { "eta_max": 60, "eta_min": 45, "data_science_eta_max": 63, "data_science_eta_min": 44 }, "15": { "eta_max": 60, "eta_min": 45, "data_science_eta_max": 63, "data_science_eta_min": 44 }, "30": { "eta_max": 60, "eta_min": 45, "data_science_eta_max": 63, "data_science_eta_min": 44 }, "45": { "eta_max": 60, "eta_min": 45, "data_science_eta_max": 63, "data_science_eta_min": 44 }, "60": { "eta_max": 60, "eta_min": 45, "data_science_eta_max": 63, "data_science_eta_min": 44 } } } ``` DynamoDb does support nested attributes up to 32 levels deep so something like this would be possible. **Question** - How big would a full schedule be? looks like Dynamodb items can be up to 400kb ```json= { "shop_id": 101, "delivery_schedule": { "Monday": { "0": { "eta_max": 60, "eta_min": 45, "data_science_eta_max": 63, "data_science_eta_min": 44 }, "15": { "eta_max": 60, "eta_min": 45, "data_science_eta_max": 63, "data_science_eta_min": 44}, "30": { "eta_max": 60, "eta_min": 45, "data_science_eta_max": 63, "data_science_eta_min": 44 }, "45": { "eta_max": 60, "eta_min": 45, "data_science_eta_max": 63, "data_science_eta_min": 44}, "60": { "eta_max": 60, "eta_min": 45, "data_science_eta_max": 63, "data_science_eta_min": 44 } }, "Tuesday": { "0": { "eta_max": 60, "eta_min": 45, "data_science_eta_max": 63, "data_science_eta_min": 44 }, "15": {}, "30": {}, "45": {}, "60": {} }, "Wednesday": { "0": { "eta_max": 60, "eta_min": 45, "data_science_eta_max": 63, "data_science_eta_min": 44 }, "15": {}, "30": {}, "45": {}, "60": {} }, "Thursday": { "0": {}, "15": {}, "30": {}, "45": {}, "60": {} }, "Friday": { "0": {}, "15": {}, "30": {}, "45": {}, "60": {} }, "Default": { "eta_max": 60, "eta_min": 45 } } } ``` Without being super well versed in DynamoDb, it would seem like we would load the full schedule and then access our desired values by key. I _think_ we can use secondary indices to access nested values. Need to investigate further #### Insert into DynamoDB Example Note that the DynamoDB Workbench seems to want you to use `'` rather than `"` to enclose keys and strings. ```sql= INSERT INTO "shop_eta_schedules_2" VALUE { 'shop_id': 101, 'day': 'Monday', 'delivery_schedule': { 'default': { 'eta_max': 50, 'eta_min': 35 }, '0': { 'eta_max': 60, 'eta_min': 45, 'data_science_eta_max': 63, 'data_science_eta_min': 44 }, '15': { 'eta_max': 60, 'eta_min': 45, 'data_science_eta_max': 63, 'data_science_eta_min': 44 }, '30': { 'eta_max': 60, 'eta_min': 45, 'data_science_eta_max': 63, 'data_science_eta_min': 44 }, '45': { 'eta_max': 60, 'eta_min': 45, 'data_science_eta_max': 63, 'data_science_eta_min': 44 }, '60': { 'eta_max': 60, 'eta_min': 45, 'data_science_eta_max': 63, 'data_science_eta_min': 44 } } }; ``` #### Update single item using boto3 client ```python= client = boto3.client( "dynamodb", aws_access_key_id="dummy", aws_secret_access_key="dummy", endpoint_url="http://localhost:8000", region_name="dummy", ) client.update_item( TableName="Schedules", Key={ "shop_id": {"N": "101"}, "day": {"S": "Monday"} }, UpdateExpression="SET #schedule.#interval.#field = :update_value", ExpressionAttributeNames={ "#schedule": "delivery_schedule", "#interval": "1380", "#field": "eta_max" }, ExpressionAttributeValues={ ":update_value": {"N": "99"} } ) ``` #### Query table using boto3 resource ```python= ddb = boto3.resource( "dynamodb", aws_access_key_id="dummy", aws_secret_access_key="dummy", endpoint_url="http://localhost:8000", region_name="dummy", ) table = ddb.Table("Schedules") response = table.query( KeyConditionExpression=Key("shop_id").eq(101) & Key("day").eq("Monday") ) ## accessing interval 1380 in response result = response["Items"][0]["delivery_schedule"]["1380"] ``` #### Read shop's eta example ```python= # Pseudo Code based on the actual select statement schedule = "SELECT * FROM shop_eta_schedules_2 WHERE shop_id = 101 and day='Monday'" current_minute = 48 current_15_min_interval = str(15 * round(current_minute/15)) current_schedule = schedule['delivery_schedule'].get(current_15_min_interval, {}) eta_max = current_schedule.get('eta_max') v2_eta_max = current_schedule.get('data_science_eta_max') default_eta = schedule['delivery_schedule'].get('default', {}).get('eta_max') ``` #### Python Libraries ##### PynamoDB [PynamoDB](https://github.com/pynamodb/PynamoDB) seems to be a good choice for a dynamo library, based on experimentation I've done with it. The other main alternative being the AWS SDK, which is quite verbose in comparison. Pynamo gives us something similar to the elasticsearch-dsl library, is less verbose, and more ORM-esque in its usage. ##### pymongo [pymongo](https://github.com/mongodb/mongo-python-driver) is the library available for mongoDB. It overal seems pretty intuitive, and comes with some potentially useful tools with stuff like timezone support built in. ### RDS TODO: Investigate RDS ## What order eta events should trigger message Shortcut ticket - https://app.shortcut.com/slicelife/story/297356/admin-investigate-which-order-events-should-trigger-an-order-eta-message #### Admin - Post Order Eta Reads (places where `@order.order_eta` is called): - https://github.com/slicelife/myr-admin/blob/master/app/jobs/receipt_transmit_job.rb#L16 - https://github.com/slicelife/myr-admin/blob/master/app/jobs/receipt_transmit_job.rb#L26 - https://github.com/slicelife/myr-admin/blob/master/app/views/customer_mailer/google_order_adjusted.html.erb#L176 - https://github.com/slicelife/myr-admin/blob/master/app/views/customer_mailer/new_order_email.html.erb#L132 - https://github.com/slicelife/myr-admin/blob/master/app/views/customer_mailer/order_adjusted.html.erb#L15 - https://github.com/slicelife/myr-admin/blob/master/app/views/customer_mailer/order_transmission.html.inky#L334 - https://github.com/slicelife/myr-admin/blob/master/app/views/customer_mailer/order_transmission.html.inky#L336 - https://github.com/slicelife/myr-admin/blob/master/app/views/customer_mailer/partials/_order_eta.html.inky#L7 - https://github.com/slicelife/myr-admin/blob/master/app/views/customer_mailer/partials/_order_eta.text.erb#L8 - https://github.com/slicelife/myr-admin/blob/master/app/views/orders/partials/_details.html.erb#L228 - https://github.com/slicelife/myr-admin/blob/master/app/views/orders/partials/_quick_info.html.erb#L19 - https://github.com/slicelife/myr-admin/blob/master/lib/eta/order_format_helper.rb#L9-L10 - https://github.com/slicelife/myr-admin/blob/master/lib/orders/etas/creation.rb#L19 - https://github.com/slicelife/myr-admin/blob/master/lib/orders/progress_steps/creation.rb#L41 - https://github.com/slicelife/myr-admin/blob/master/lib/services/delivery/adapters/delivery_request.rb#L116 - https://github.com/slicelife/myr-admin/blob/master/lib/sms/customer_receipt.rb#L25 - https://github.com/slicelife/myr-admin/blob/master/lib/sms/transactional/eta_updated.rb#L28 Writes (where order eta messages should be triggered?): - https://github.com/slicelife/myr-admin/blob/master/app/controllers/orders_controller.rb#L381 - https://github.com/slicelife/myr-admin/blob/master/app/controllers/orders_controller.rb#L538 - https://github.com/slicelife/myr-admin/blob/master/app/controllers/voice_instructions_controller.rb#L80 - https://github.com/slicelife/myr-admin/blob/master/app/controllers/voice_instructions_controller.rb#L89 - https://github.com/slicelife/myr-admin/blob/master/lib/orders/etas/creation.rb#L20-L21 ## Sample Shop Eta Schedule Data Some examples of shop eta schedule data in various format to experiment with ### Single Arrays <details> <summary>Click to expand</summary> ```json= { "shop_id": 101, "max_delivery": [ 40, 40, 35, 35, 35, 35, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 35, 35, 35, 35, 35, 35, 35, 40, 40, 40, 35, 35, 35, 40, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 40, 35, 35, 35, 35, 35, 35, 40, 40, 40, 35, 35, 35, 35, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 35, 35, 35, 35, 35, 35, 35, 40, 40, 40, 35, 35, 35, 40, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 40, 35, 35, 35, 35, 35, 35, 40, 40, 40, 35, 35, 35, 40, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 35, 35, 35, 35, 35, 35, 40, 45, 45, 45, 40, 35, 35, 35, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 40, 35, 35, 35, 35, 35, 35, 40, 45, 40, 35, 35, 35, 35, 55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 40, 35, 35, 35, 35, 35, 40, 40 ], "min_delivery": [ 25, 25, 20, 20, 20, 20, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 20, 20, 20, 20, 20, 20, 20, 25, 25, 25, 20, 20, 20, 25, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 25, 20, 20, 20, 20, 20, 20, 25, 25, 25, 20, 20, 20, 20, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 20, 20, 20, 20, 20, 20, 20, 25, 25, 25, 20, 20, 20, 25, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 25, 20, 20, 20, 20, 20, 20, 25, 25, 25, 20, 20, 20, 25, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 20, 20, 20, 20, 20, 20, 25, 30, 30, 30, 25, 20, 20, 20, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 25, 20, 20, 20, 20, 20, 20, 25, 30, 25, 20, 20, 20, 20, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 25, 20, 20, 20, 20, 20, 25, 25 ] } ``` </details> ### Array of tuples <details> <summary>Click to expand</summary> ```json= { "shop_id": 101, "delivery_eta": [ [ 25, 40 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 25, 40 ], [ 25, 40 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 25, 40 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 25, 40 ], [ 25, 40 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 25, 40 ], [ 25, 40 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 25, 40 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 25, 40 ], [ 25, 40 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 25, 40 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 25, 40 ], [ 30, 45 ], [ 30, 45 ], [ 30, 45 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 25, 40 ], [ 30, 45 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 25, 40 ], [ 25, 40 ] ] } ``` </details> ### Matrix <details> <summary>Click to expand</summary> ```json= { "shop_id": 101, "delivery_eta": [ [ [ 25, 40 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 25, 40 ] ], [ [ 25, 40 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 25, 40 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 25, 40 ] ], [ [ 25, 40 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 25, 40 ] ], [ [ 25, 40 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 25, 40 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 25, 40 ] ], [ [ 25, 40 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 25, 40 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 25, 40 ], [ 30, 45 ] ], [ [ 30, 45 ], [ 30, 45 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 25, 40 ] ], [ [ 30, 45 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 40, 55 ], [ 25, 40 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 20, 35 ], [ 25, 40 ], [ 25, 40 ] ] ] } ``` </details> ### Composite Eta Ranges. `to`/`from` in minutes of day #### No overlaping ranges ```json= { "shop_id": 101, "default_eta_max": 40, "default_eta_min": 55, "delivery_etas": [ { "day": "Monday", "from": 0, "to": 120, "eta_max": 55, "eta_min": 40 }, { "day": "Monday", "from": 121, "to": 600, "eta_max": 55, "eta_min": 40 }, { "day": "Monday", "from": 601, "to": 780, "eta_max": 55, "eta_min": 40 }, { "day": "Monday", "from": 781, "to": 1200, "eta_max": 75, "eta_min": 60 }, { "day": "Monday", "from": 1201, "to": 1440, "eta_max": 75, "eta_min": 60 }, { "day": "Tuesday", "from": 0, "to": 120, "eta_max": 55, "eta_min": 40 }, { "day": "Tuesday", "from": 121, "to": 600, "eta_max": 55, "eta_min": 40 }, { "day": "Tuesday", "from": 601, "to": 780, "eta_max": 55, "eta_min": 40 }, { "day": "Tuesday", "from": 781, "to": 1200, "eta_max": 75, "eta_min": 60 }, { "day": "Tuesday", "from": 1201, "to": 1440, "eta_max": 75, "eta_min": 60 }, { "day": "Wednesday", "from": 0, "to": 120, "eta_max": 55, "eta_min": 40 }, { "day": "Wednesday", "from": 121, "to": 600, "eta_max": 55, "eta_min": 40 }, { "day": "Wednesday", "from": 601, "to": 780, "eta_max": 55, "eta_min": 40 }, { "day": "Wednesday", "from": 781, "to": 1200, "eta_max": 75, "eta_min": 60 }, { "day": "Wednesday", "from": 1201, "to": 1440, "eta_max": 75, "eta_min": 60 }, { "day": "Thursday", "from": 0, "to": 120, "eta_max": 55, "eta_min": 40 }, { "day": "Thursday", "from": 121, "to": 600, "eta_max": 55, "eta_min": 40 }, { "day": "Thursday", "from": 601, "to": 780, "eta_max": 55, "eta_min": 40 }, { "day": "Thursday", "from": 781, "to": 1200, "eta_max": 75, "eta_min": 60 }, { "day": "Thursday", "from": 1201, "to": 1440, "eta_max": 75, "eta_min": 60 }, { "day": "Friday", "from": 0, "to": 120, "eta_max": 55, "eta_min": 40 }, { "day": "Friday", "from": 121, "to": 600, "eta_max": 55, "eta_min": 40 }, { "day": "Friday", "from": 601, "to": 780, "eta_max": 55, "eta_min": 40 }, { "day": "Friday", "from": 781, "to": 1200, "eta_max": 75, "eta_min": 60 }, { "day": "Friday", "from": 1201, "to": 1440, "eta_max": 75, "eta_min": 60 }, { "day": "Saturday", "from": 0, "to": 120, "eta_max": 55, "eta_min": 40 }, { "day": "Saturday", "from": 121, "to": 600, "eta_max": 55, "eta_min": 40 }, { "day": "Saturday", "from": 601, "to": 780, "eta_max": 55, "eta_min": 40 }, { "day": "Saturday", "from": 781, "to": 1200, "eta_max": 75, "eta_min": 60 }, { "day": "Saturday", "from": 1201, "to": 1440, "eta_max": 75, "eta_min": 60 }, { "day": "Sunday", "from": 0, "to": 120, "eta_max": 55, "eta_min": 40 }, { "day": "Sunday", "from": 121, "to": 600, "eta_max": 55, "eta_min": 40 }, { "day": "Sunday", "from": 601, "to": 780, "eta_max": 55, "eta_min": 40 }, { "day": "Sunday", "from": 781, "to": 1200, "eta_max": 75, "eta_min": 60 }, { "day": "Sunday", "from": 1201, "to": 1440, "eta_max": 75, "eta_min": 60 } ] } ``` #### Overlapping ranges from different sources ```json= [{ "shop_id": 101, "default_eta_max": 40, "default_eta_min": 55, "delivery_etas": [ { "day": "Monday", "from": 0, "to": 120, "eta_max": 55, "eta_min": 40, "source": "shop_set" }, { "day": "Monday", "from": 60, "to": 120, "eta_max": 65, "eta_min": 50, "source": "data_science_v2" }, { "day": "Monday", "from": 121, "to": 600, "eta_max": 55, "eta_min": 40 } ] }, { "shop_id": 102, "default_eta_max": 40, "default_eta_min": 55, "delivery_etas": [] }] ``` ### Composite Eta Ranges using ISODate timestamps #### No overlapping ranges ```json= { "shop_id": 101, "default_eta_max": 40, "default_eta_min": 55, "delivery_etas": [ { "day": "Monday", "from": ISODate("2022-06-06T00:00:00Z"), "to": ISODate("2022-06-06T02:00:00Z"), "eta_max": 55, "eta_min": 40 }, { "day": "Monday", "from": ISODate("2022-06-06T02:00:01Z"), "to": ISODate("2022-06-06T10:00:00Z"), "eta_max": 55, "eta_min": 40 }, { "day": "Monday", "from": ISODate("2022-06-06T10:00:01Z"), "to": ISODate("2022-06-06T13:00:00Z"), "eta_max": 55, "eta_min": 40 }, { "day": "Monday", "from": ISODate("2022-06-06T13:00:01Z"), "to": ISODate("2022-06-06T17:00:00Z"), "eta_max": 75, "eta_min": 60 }, { "day": "Monday", "from": ISODate("2022-06-06T17:00:01Z"), "to": ISODate("2022-06-06T23:59:59.999Z"), "eta_max": 75, "eta_min": 60 } ] } ```