Skip to content

What this holds about a person

8 tables, 12 columns about a person, 3 things held outside a column, 1 kind of data subject, 3 destinations. Some of it is kept indefinitely, and every one of those says why below.

What it holds

delivery_date_booking

Who a row is about: the shopper, by order_id matched into order.order_id, which names them at email.

What makes a row go away: never — Nothing on a clock removes a booking, and the reason is the same decision that makes this extension need no listener for a deleted order: released is a predicate over the joined oc_order, evaluated by the read that needs it, so a booking whose order is gone satisfies no predicate and is invisible to every query this extension makes rather than wrong in any of them. What survives is an orphan row holding a date and a window with nothing tying it to a person — unreachable by an access request, because the join such a request arrives on no longer resolves, and holding nothing that would identify anybody if it were read. Saying that plainly is more honest than a sweep that would claim to erase something already unlinked. While the order is still there, the row goes when somebody asks: that is the button, not the clock.

Column What it is Why it exists What an erasure does
order_id refers Which order is being delivered, and the whole of how a person is reached from this table. It is the primary key rather than an order_product_id, because core destroys and recreates an order's child rows on every confirm render and a key re-minted underneath a live booking would re-point it at somebody else's delivery. delete
state about Whether this order has a delivery day at all — scheduled or unscheduled. There is deliberately no third value mirroring the order's own status, which is what makes the rule self-healing. delete
delivery_date about The day this person was promised their order, and the column every capacity count and the whole day planner group on. delete
slot_id about Which window on that day, as the row the capacity claim locks. The name is snapshotted beside it because this id resolves to today's configuration and the name does not. delete
slot_name about What the window was called when this person chose it, resolved in the order's own language and frozen there — and written again, the same way, when the merchant reassigns the booking to another window from the admin order page. A merchant renaming a window afterwards changes what the picker offers from then on and must not change what this customer was told they were getting. delete
method_reference about Which shipping method the delivery was booked against, frozen as the <extension>.<quote_key> reference the calendar was bound by — so a merchant re-binding a calendar, or a third-party method minting a different key next quarter, does not rewrite which method this delivery was arranged under. delete
chosen_date about The day this person actually picked, written only where firming had to move them off it — so a value here means this booking changed after the customer stopped looking, and the ordinary case stores nothing at all. delete
chosen_slot_name about The window they actually picked, written by the same path and for the same reason, so a merchant answering why is my delivery not when I chose it has both halves of the answer. delete
touched_at about When this person was last seen holding this window, which is the input to the hold predicate and therefore the reason ten shoppers in checkout at once are not all offered the last slot. delete
firmed_at about When the booking stopped being a hold and became a promise, which is what a capacity count measures a confirmed order by rather than by the order's own status. delete
date_added about When the delivery was first arranged, which is what makes when was this booked answerable months later against a calendar that has since changed. delete
date_modified about When anything about this person's delivery last changed — the hold, the firming, a move on the planner or their order's status — which is what lets a carrier's system ask for the bookings that changed since it last looked instead of re-reading every one. delete

What it holds that is not in a column

A file, a cookie and a key in the session are exactly where an erasure written as row deletion reaches nothing, and no schema can be diffed to find one. Each is listed here with what keys it to a person, why it is there, and what removes it.

A file on disk. kyvero.log in the store's own DIR_LOGS, the one diagnostic file every extension in this repository shares, capped at 1 MiB and trimmed oldest-first.

Nothing keys this to a person — every message is put through the diary's redaction rule before a byte is written, which is what makes the file survivable where OpenCart puts its own error log.

Why it is here: A merchant whose bookings stopped being written needs one place to look, and a support conversation without it is guesswork on both sides.

What an erasure does: retain.

A file on disk. Not a file at all: a php://temp memory stream, opened for the length of one request when a merchant presses Export on the day planner and discarded with it. What goes into it is the day's sheet — order id, customer name, delivery address, window and status — and what comes out of it is the CSV that merchant downloads. Nothing is written to disk by this extension at any point, and there is no path anywhere in either line.

What keys it to a person: The order ids of whichever day the merchant asked for, read live out of the store at the moment they pressed the button.

Why it is here: The sheet has to leave the room: a delivery round is driven by somebody holding a printed or downloaded list, and a planner that could only be read on a screen in the office is a planner nobody uses.

What an erasure does: retain.

A key in the store's session. The store's session, under the key the checkout controller spells self::SESSION — delivery_date. It holds the day and the window this shopper has chosen while they are still in checkout — before there is an order to write a booking against.

It does not outlive the request that wrote it: this is session state, gone when the session is, without a clock having to run or a request having to reach it.

Nothing keys this to a person — it is keyed by the session itself and carries no address, no name and no customer id. Who the session belongs to is the store's own business and not written here.

Why it is here: The choice is made on the shipping step and the order does not exist until the confirm page renders, so it has to be held somewhere across those requests. The alternative — a row of our own keyed on a session id — would turn every abandoned checkout into a durable record of what somebody nearly bought and when they gave up.

What an erasure does: delete.

What it deliberately does not hold

Each of these is a column that could have been stored and was not, with the reason it was not. They are decisions rather than omissions.

A name, an address, an email or a telephone number on the booking — any copy at all of who the delivery is for.

Every read path already joins oc_order for the status, so a copy would buy no query and no screen anything, and it would turn a table of dates and windows into a second address book that no merchant knows they have. It is also what makes an orphaned booking harmless: with nothing identifying on the row, a booking whose order has been deleted holds a date and a window and nothing else. The one thing the copy would buy is an erasure that could match without a join — which is collecting personal data in order to protect it.

A denormalised order status on the booking, so released could be read without a join.

It is the same refusal twice. A copy of the status is a copy that drifts the moment a merchant changes the order in any of the four places core lets them, and the rule it would serve is better as a predicate over the joined order — which is what makes it self-healing, and what makes a deleted order need no listener of its own rather than a sweep chasing rows nobody can see.

A free-text instruction from the shopper — leave it with next door, ring the top bell.

It is the obvious next field and it is deliberately absent, because it is the field that would break the argument above. Every column here is an input to a predicate, which is why a booking with no order left means nothing; a sentence somebody typed still means what it meant with no order at all, and it can carry a neighbour's name, a door code or a health reason nobody asked for. Core already has oc_order.comment for what a shopper wants to say, where a merchant already knows to look for it.

A session id, or anything else identifying, on the chosen day and window held during checkout.

The session key holds a date and a window and nothing else, which is why it can be described as holding nothing about who chose them. Writing the shopper into it — or persisting it as a row keyed on a session id, which is the version somebody always proposes for analytics — would make every abandoned checkout a durable record of what somebody nearly bought and when they gave up.

Where it goes

The store's own mail transport, whichever one the merchant configured in OpenCart — mail() or their own SMTP server. The delivery line is appended to core's own order confirmation and invoice mails rather than sent as a message of its own, so what leaves is one extra sentence inside a mail the store was already sending to that customer.

Chosen by: merchant. What reaches it: delivery_date_booking.delivery_date, delivery_date_booking.slot_name.

The merchant's own staff, in their own browser: the day planner's screen, its print sheet and the CSV it exports, each of which is the delivery round for one day. It goes no further than whoever the merchant gave access on extension/delivery_date/sale/planner to — a permission of its own, so the sheet can be given to a warehouse hand without giving them every module in the store.

Chosen by: merchant. What reaches it: delivery_date_booking.order_id, delivery_date_booking.delivery_date, delivery_date_booking.slot_name, delivery_date_booking.method_reference, delivery_date_booking.chosen_date, delivery_date_booking.chosen_slot_name, delivery_date_booking.state.

Core's own printed invoice and shipping list (sale/order.invoice, sale/order.shipping), printed by staff with access on sale/order, which is paper that leaves the building with the parcel.

Chosen by: merchant. What reaches it: delivery_date_booking.delivery_date, delivery_date_booking.slot_name, delivery_date_booking.state.

Every destination above is one you configured — your own mail transport, your own API caller presenting your own credential. Nothing goes anywhere this extension chose: a destination we picked that anything personal reached would fail the build, not by default, not behind a setting and not with a warning.

What the standard asks, and what this extension answers

Kept indefinitely, on purpose. Nothing below is removed by a clock. Each one says why, which is the part a merchant relying on it has to be able to state:

  • delivery_date_booking — Nothing on a clock removes a booking, and the reason is the same decision that makes this extension need no listener for a deleted order: released is a predicate over the joined oc_order, evaluated by the read that needs it, so a booking whose order is gone satisfies no predicate and is invisible to every query this extension makes rather than wrong in any of them. What survives is an orphan row holding a date and a window with nothing tying it to a person — unreachable by an access request, because the join such a request arrives on no longer resolves, and holding nothing that would identify anybody if it were read. Saying that plainly is more honest than a sweep that would claim to erase something already unlinked. While the order is still there, the row goes when somebody asks: that is the button, not the clock.

What reaches them instead is the remove-everything screen, which is a request you act on rather than a clock that runs.

Each row below is keyed by the sub-paragraph of the General Data Protection Regulation it comes from, so that you can read the source and disagree with us. What each state means is on the data-protection boundary, once, rather than reworded here. declared is not a pass: it says what the thing is, not that the thing is fine.

Article What this extension supplies toward it This extension
5(1)(c) Every column this extension can put in a store is written down with the one sentence saying why it is there, and a column that is not fails the build — so what a store keeps is what somebody decided to keep rather than what accumulated. Beside it, in the same file and the extension's own voice, is what it deliberately does not keep, and why. checked, 44 — every column this extension's schema can put in a store
30(1)(c) What this extension holds about a person is published column by column — what the column is, whether it names somebody or points at them, and why it exists — so the record a merchant has to keep can be copied off a page rather than reconstructed out of the database. declared
7(1) Where anything this extension does rests on somebody having agreed to it, the declaration names the wording they agreed to and where the proof of it is recorded — and where nothing rests on consent it says so, because we did not need any is an answer and a blank is not. declared
7(3) A declared consent carries the path by which it is withdrawn, or the build fails — because withdrawing has to be no harder than giving, and a consent whose withdrawal path nobody wrote down is one a merchant discovers they cannot honour on the day somebody asks. checked, 0 — every consent declared anywhere in it
15(1) Every table holding anything about a person answers who that person is and how a request reaches them — a column on the table itself, the path through a table this extension does not own, or the plain statement that nothing on it keys a row to anybody — so that a request either has somewhere to arrive or is told outright that there is nowhere, rather than a screen having to guess which rows are whose. checked, 1 — every table holding anything about a person
16(1) A column holding a frozen copy of something the store holds elsewhere names the column it was copied from, so that correcting the original is an instruction a merchant can follow rather than a shrug about why the two disagree. declared
21(3) The row that records somebody saying stop names the subject it is keyed on and the verdict an erasure gives it, so that a live instruction to stop mailing survives the request that was meant to enforce it rather than being deleted by it. declared
25(2) A column that is only collected when a merchant turns something on names the setting that decides it, and the shipped value is read off the configuration declaration rather than restated here — so what a store collects out of the box is a fact on a page instead of something read out of a controller. declared
5(1)(e) Every table this extension can put in a store answers what makes a row holding a person go away — one of six verdicts, and a table that answers 'never' says why in the same breath or fails the build — so a store keeping something for ever is keeping it on purpose. checked, 1 — every table an answer is owed for
15(1)(d) The period each table is kept for is published beside what it holds, as the mechanism and — where there is one — the number or the setting it is read from, so that a merchant answering somebody who asks how long their data will be stored is copying an answer rather than composing one. declared
17(3) Everything an erasure deliberately keeps is published with the reason it was kept, and a kept column with no reason beside it fails the build — because the exemption a merchant relies on is one they have to be able to state, and the screen states it to them at the moment they press the button. declared
17(1) A merchant erases one named person from a screen, and what happens is what the declaration said would happen: the rows and the files an erasure takes are gone, what it keeps is still there, the person standing beside them is untouched, and pressing it a second time is safe. Asserted by running it on a real store — the only place in this standard where erasure behaviour is ever observed, and the only place a file is. Asserted on a real store by make smoke, and deliberately not here — this page is generated by make check, which is green with no store at all, so a verdict rendered from it would rest on nothing.
30(1)(f) The envisaged time limits for erasure are published per table rather than per extension, so the line a merchant copies into their own record says which data it is about instead of averaging seven answers into one. declared
15(1)(c) Every destination this extension's holdings leave it for is published by name, with which of those holdings reach it — so answering somebody who asks who their data was disclosed to is reading a page rather than reading the source. declared
20(2) This extension transmits nothing directly to another controller, and says so out loud rather than leaving it unmentioned: the destinations that exist are the published list, an export is a file the merchant receives and hands on themselves, and there is no path by which we send one controller's data to another on their behalf. declared
30(1)(d) Every destination is written down with who chose it, and a destination this extension chose itself that anything personal reaches fails the build — not by default, not behind a setting, not with a warning, because a store owner cannot consent on behalf of the people in the file. checked, 3 — every destination anything leaves it for
15(3) The screen that says what is held about one named person also hands that statement over: one button produces a file carrying every holding it just listed — what is held, why it is there and what removes it — so answering somebody who asked is sending what the screen showed rather than retyping it into an email. What the file does not carry is the values themselves, which stay in the store; 20(1) below says what that costs. Every extension ships that screen and that button byte for byte, so the file is the same file whichever extension a merchant happened to open. checked, 13 — every holding the file carries
20(1) That file is JSON — structured, commonly used and machine-readable — rather than a screen printed to paper, so whoever asked for it can read it with something other than their eyes. What it carries is the inventory and not the contents: every holding this extension has about that person, folded out of the declaration without a table being opened, which is why two people's files differ in the address at the top and nowhere else. Portability is the right to receive the data itself, and this is not that — the values are in the merchant's own database, and what this supplies is a machine-readable statement of where each one is and what removes it. The row is declared for that reason and not machine: what the rule behind it decides is that every extension ships one export byte for byte, which says what the file is and not that the file is what this article asks for. declared
30(1)(g) The technical and organisational measures are the security baseline's subject, published on its own page with its own rows and its own scanners, and this page links to it rather than restating any of it. What is asserted here is the seam between them: every file this extension writes is declared in both places and cross-checked both ways, so the two descriptions cannot drift apart. declared
KYV-D1 Everything this extension holds about a person can be removed on purpose, from a screen, without uninstalling it — and the screen shows what will go before it goes. Uninstalling keeps it all, because an OpenCart upgrade is an uninstall followed by an install and an extension that dropped its tables on the way out would destroy a store's data on every routine update. A holding every one of whose columns is kept for ever, with nothing anywhere able to remove any of it, fails the build. checked, 12 — every holding the remove-everything screen reaches

What is left over

3 columns here are frozen copies of things the store already holds: delivery_date_booking.slot_name from delivery_date_slot_description.name, delivery_date_booking.method_reference from delivery_date_calendar_method.reference, delivery_date_booking.chosen_slot_name from delivery_date_slot_description.name. Correcting one of those is done at the source and not here: the copy is kept deliberately, so that deleting the original does not delete the record that was made from it.

Removing all of it is one button, on Delivery Date's own settings screen at Admin > Extensions > Extensions > Modules > Delivery Date. It prints what will go — every table, every file, every key, with a count beside each line and the reason beside the ones it keeps — and nothing goes until you press the second button. It cannot be undone: no soft delete, no recycle bin, no undo, because a recycle bin for personal data is personal data that is still there. Uninstalling does not do it, deliberately: an OpenCart upgrade is an uninstall followed by an install, so an extension that dropped its tables on the way out would destroy your data every time you updated it. Your settings, your configuration and whatever the extension remembers about itself are not touched.

What the law asks of you — you are the controller, and installing an extension is not a compliance process — is on the data-protection boundary, which is also where the words above are defined and where it says why there is no badge. What OpenCart's own erasure feature does underneath all of this, and the things about it worth knowing first, is on what core's own GDPR feature does.