Skip to content

What this holds about a person

9 tables, 32 columns about a person, 6 things held outside a column, 2 kinds of data subject, 3 destinations. Some of it is kept indefinitely, and every one of those says why below.

What it holds

preflight_plan

Who a row is about: the shopper, by match_value on this table.

What makes a row go away: swept — The plan is half of what makes an import reversible, and it is also half of what grows without limit: a daily feed writes one every morning. The cron purge deletes every row of a job older than the window and marks that job no longer reversible, so the history says what happened even once the undo has gone. The window is the merchant's: thirty days out of the box, and a value of zero keeps every plan for good. Nothing here overrides that — a store owner who wants an undo that never expires has said so — and what the page does instead is render whichever answer this store actually holds.

How long: the module_preflight_retention setting — How many days a job's plan and journal are kept for. A scheduled purge throws away anything older, and a job whose data has gone is marked in the history as no longer reversible. 0 keeps every job's rollback data until it is purged on request. A store that has not changed it answers 30. You set it at Admin > Extensions > Extensions > Modules > Import/export.

Column What it is Why it exists What an erasure does
match_value identifies The value the source row was matched on, which is whatever the merchant chose to match by: an email address when matching customers, an order number when matching orders, a part number when matching products. Declared as identifying on every row because it is identifying on some of them and no static declaration can tell which. delete
record_id refers The core row this op would write — a customer_id, an order_id, a product_id. Identity rather than a rule key: it is what a rollback puts back, and for a customer import it is whose account this op was about. delete
before_values identifies The whole field image of the core record as it stood before this op, as one text column. For a customer that is name, email, telephone, IP and addresses; for an order it is all of that plus both full addresses, the comment, the forwarded IP, the user agent and the accept-language. It is not a copy of one column but of a whole record, which is the second grammar snapshot_of takes. delete
after_values identifies The same image as this op would leave it. A plan holds both halves so a merchant reads the change rather than the result, and so a rollback has somewhere to go back to. delete
error about Why this row was refused, as the sentence a merchant reads beside it. About the row rather than about the person, and meaningless away from it. delete
error_field about Which mapped field was refused, by name. The field name is the merchant's mapping and not anybody's data; it is declared with the pair it belongs to rather than split off under none. delete
error_value identifies The value that was refused, kept whole in a text column so the merchant can see what their file actually said. On a customer import that is the address or the telephone number the file got wrong, which makes this the one column where a rejected row holds more than an accepted one. delete
date_added about When this op was planned, which is also the retention clock's input: the sweep measures a job's age and this is what a row of it was written at. delete

preflight_journal

Who a row is about: the shopper, by match_value on this table.

What makes a row go away: swept — The journal is the other half of the undo, and it goes with the plan on the same window and in the same pass — a job whose plan survived and whose journal did not would be a rollback that half happens. The window is the merchant's, zero keeps everything, and the page renders whichever answer this store holds rather than the one shipped.

How long: the module_preflight_retention setting — How many days a job's plan and journal are kept for. A scheduled purge throws away anything older, and a job whose data has gone is marked in the history as no longer reversible. 0 keeps every job's rollback data until it is purged on request. A store that has not changed it answers 30. You set it at Admin > Extensions > Extensions > Modules > Import/export.

Column What it is Why it exists What an erasure does
entity about Which of the fifteen importable entities this row was about — and therefore the column that decides whether the rest of the row is about a person at all. It is declared with the classified columns rather than under none precisely because it is the one a reader has to look at to know what they are looking at. delete
match_value identifies The value this record was matched on when it was written, kept on the journal as well as on the plan so a rollback does not depend on a plan that may have been purged from under it. delete
record_id refers The core row this write touched, which is what a rollback addresses when it puts the before image back. delete
before_values identifies The whole field image as it stood the instant before this write — the most sensitive single column in this repository, because for an order it is a complete second copy of somebody's order, addresses and all, sitting in a table core knows nothing about. delete
after_values identifies What was written, kept beside what was replaced so a rollback can refuse to undo a record somebody has since edited by hand. delete
date_added about When this write happened, and the clock the sweep measures the job by. delete

preflight_tally

Who a row is about: the shopper, by sample on this table.

What makes a row go away: swept — A tally belongs to its plan and goes with it, in the same pass and on the same merchant-set window. It is summary rather than undo, and keeping a summary of a plan that has been thrown away would be keeping the part that names people and discarding the part that explains them.

How long: the module_preflight_retention setting — How many days a job's plan and journal are kept for. A scheduled purge throws away anything older, and a job whose data has gone is marked in the history as no longer reversible. 0 keeps every job's rollback data until it is purged on request. A store that has not changed it answers 30. You set it at Admin > Extensions > Extensions > Modules > Import/export.

Column What it is Why it exists What an erasure does
sample identifies One example of what this field is about to be changed to, so a merchant reading 312 records change email can see what one of them looks like before they agree to it. Conditionally personal in exactly the way the plan's five columns are, and declared at its worst case for the same reason. delete

preflight_seen

Who a row is about: the shopper, by match_value on this table.

What makes a row go away: swept — One row per source row is the largest table a job writes, and it is thrown away with that job's plan on the merchant's own window. It answers a question about one job — what did this file account for — and a job past its window has no more questions to answer.

How long: the module_preflight_retention setting — How many days a job's plan and journal are kept for. A scheduled purge throws away anything older, and a job whose data has gone is marked in the history as no longer reversible. 0 keeps every job's rollback data until it is purged on request. A store that has not changed it answers 30. You set it at Admin > Extensions > Extensions > Modules > Import/export.

Column What it is Why it exists What an erasure does
match_value identifies The value this source row carried, whether or not it matched anything. On a customer import that is every address in the supplier's file, including the ones that matched nobody — which makes this table a list of people the store may never have heard of. delete
record_id refers The core row the value matched, or 0 for a value that matched none. It is what lets a record a job accounted for and did not need to change still be stamped with that job's label. delete

preflight_label

Who a row is about: the shopper, by record_id matched into customer.customer_id, which names them at email.

What makes a row go away: never — Nothing on any clock removes a label. The retention purge deliberately leaves it: it is not rollback data, and a record whose provenance disappeared a month after it was imported would answer where did this come from with silence. What does replace one is the next job to write that same record, because the question is where the data came from and the last job to write it is the answer. The remove-everything screen is the only thing that takes it, which is what that screen is for.

Column What it is Why it exists What an erasure does
entity refers Which kind of record this label is about, and the half of the reference that says whether record_id is a person at all. A reference is the pair or it is nothing, which is why both halves are declared and neither is under none. delete
record_id refers The record this label is about. For an imported customer or order that is a person, and the label beside it is a durable fact about where their data came from. delete
label about The batch name the operator typed when they ran the import, shown on the record's own edit form in the admin. Operator-typed, so it can name a supplier, a colleague or a third party the store holds no other record of. delete

preflight_job

Who a row is about: the staff, by user_id matched into user.user_id, which names them at username.

What makes a row go away: never — The job row outlives its own undo and is meant to: uninstalling Import/export does not drop a table, a purge sets a flag rather than deleting a row, and the counts are how a store owner answers what did we import in March a year later. Nothing removes it on a clock, and the honest consequence — that the personal columns on it would otherwise sit there for ever with no screen able to reach them — is why the remove-everything screen exists and why it empties them rather than deleting the row.

Column What it is Why it exists What an erasure does
name about What the operator called this import. Operator-typed free text, so it can carry a supplier's name, a colleague's name or anything else somebody thought would help them find it again — which is also why it is the string this extension's security baseline records as an unencoded log carrier. blank
label about The batch label stamped onto every record this job wrote, which is what preflight_label holds a copy of. Operator-typed for the same reason and with the same consequence. blank
source about Where this job read its file from — a staged upload, a path on the server, or a URL the merchant typed. A credential rides along: a URL pasted with user:password@ in its userinfo is stored exactly as it was typed, and the redaction this extension applies runs on the way into an exception message rather than on the way into this column. That is a security defect recorded elsewhere and deliberately not repaired here; what this declaration owes it is saying that the column holds it. blank
user_id refers Which member of staff planned this import. Load-bearing rather than decorative: an import nobody can be asked about is one nobody can answer for, and this is the only record of it — core keeps none. blank
applied_user_id refers Which member of staff applied it, which is frequently not the person who planned it: a job planned in the morning and applied after lunch has two authors and the second one is the one who wrote to the catalog. blank
rolled_back_user_id refers Which member of staff undid it. The third answer to who did this, and the one a store owner asks for when a catalog changed back overnight. blank

preflight_profile

Who a row is about: the staff, by user_id matched into user.user_id, which names them at username.

What makes a row go away: never — A saved feed is configuration and configuration has no expiry: a profile that vanished after thirty days would take a store's nightly import with it. It goes when the merchant deletes the feed, and not before. The remove-everything screen empties the personal columns on it and leaves the feed standing — which switches that feed off until its source is typed again, exactly as Configuration::SECRETS already leaves the provider key and the feed secret to be given again after an update, and for the same reason: the safe way round is off.

Column What it is Why it exists What an erasure does
name about What the operator called this feed. Operator-typed, carried into every run it opens, and shown in the admin — the string a person recognises their own automation by. blank
label about The batch label every run of this feed stamps onto the records it writes. Operator-typed, and the thing preflight_label ends up holding a copy of on every record. blank
source about Where this feed fetches from, typed once and used every night. The same credential-in-the-userinfo defect as the job's copy, and worse here only in that a profile is exported as a file and handed to another store — which is why the provider key is deliberately not on this table. blank
user_id refers Which member of staff set this feed up. A profile may run unattended for a year, and the only person who can say what it was for is the one who saved it. blank

preflight_run

Who a row is about: the staff, by name on this table.

What makes a row go away: swept — The run log is pruned on the same window as the rollback data it points at. It is not an undo — the jobs it names keep their counts for ever — so thinning it takes no promise with it, and a store running an hourly feed would otherwise accumulate a row an hour for as long as it kept running. A window set to keep everything keeps these too.

How long: the module_preflight_retention setting — How many days a job's plan and journal are kept for. A scheduled purge throws away anything older, and a job whose data has gone is marked in the history as no longer reversible. 0 keeps every job's rollback data until it is purged on request. A store that has not changed it answers 30. You set it at Admin > Extensions > Extensions > Modules > Import/export.

Column What it is Why it exists What an erasure does
name about The feed's name as it stood when this run started, copied rather than joined so that deleting a feed does not blank its own history. Operator-typed, and mailed to the store's own address in the subject line whenever a run needs a person. blank
error about Why this run did not finish, as the operator reads it in the admin and in the mail. It is the one place a source URL is written with its credentials removed rather than as typed — the redaction runs on the way into the exception this column is filled from — which is exactly the difference between this column and source on the job beside it. blank

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. The one thing that reaches it unencoded is an operator-typed job or feed name, which is a known Low on this extension's security page rather than a shopper's anything.

Why it is here: An import that stopped halfway through the night 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: the standard error of preflight.php, the script a person runs from a terminal. Five one-line refusals — it could not find the store's config.php, the configuration is not an OpenCart 4 one, core's vendored packages are in neither place a release keeps them, an exception got out, or the command line controller would not load.

Nothing keys this to a person — all five are fixed sentences about a store that could not be booted, written before any import has been opened and before this extension has read a row of anything.

Why it is here: A script that exits silently because it could not find the store is one a person debugs by guessing, and these are the five guesses it saves them.

What an erasure does: retain.

A file on disk. The staging area: DIR_UPLOAD . StoreFiles::STAGED and the working directory beside it, both outside the document root. Whatever the merchant handed this extension to import sits here until the job is done with it — the uploaded file itself, a remote source fetched once and written down so the rest of the job can read it locally, the images a job has downloaded and not yet installed, and the scratch file a large spreadsheet's shared-strings table is spilled into rather than held in memory. Named from uniqid() and the verified extension, never from anything a supplier's file said.

What keys it to a person: The job it was staged for, and never a person: the file is whatever the merchant supplied, so a customer import stages a file of addresses and a price list stages a file of part numbers. Nothing in the name says which.

Why it is here: An import runs in slices across separate requests, and a slice that had to re-fetch a supplier's file to carry on would be an import that changed under itself halfway through.

What an erasure does: delete.

A file on disk. The finished export, under the export directory the merchant nominated in this extension's own settings — assembled into a .part and moved into place, in whichever of the four formats was asked for. A merchant who points that directory inside their document root has made it public; nothing here does.

What keys it to a person: The job that wrote it. An export of products holds nothing about anybody; an export of reviews holds their authors' names, and the three entities whose export would be worst — orders, customers and coupons — are refused outright by the feed that serves them.

Why it is here: An export a merchant asked for has to be somewhere they can fetch it, and a job that writes one slice at a time across several requests has to append to a file rather than hold the whole thing in memory.

What an erasure does: delete.

A file on disk. The file journal: for every image an apply is about to replace, a copy of the one that was there, and for every image it is about to add, an empty marker saying nothing was there. Under the working directory, outside the document root. It is the file half of the undo, and it is thrown away by the same purge that throws away the row half.

What keys it to a person: The job whose rollback would need it. A catalog image is not personal data; this entry is here because the population is every write site and the honest answer to what is this file is better than an omission.

Why it is here: An apply that could not be rolled back because the image it replaced was gone would be an undo that half works, which is worse than one that refuses.

What an erasure does: delete.

A file on disk. The images an import installed, under DIR_IMAGE and inside the document root, where a browser fetches them directly — plus the two writes a rollback makes there, restoring the image a job replaced or deleting one it added. The extension is decided from the bytes and never from the source's name or a server-supplied content type.

What keys it to a person: The catalog record it belongs to. This is a merchant's own catalog data — a product photograph — and it is theirs rather than ours the moment it is installed.

Why it is here: An import that could bring in a supplier's product data but not their photographs would leave half the catalog to be filled in by hand.

What an erasure does: retain.

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 password, a password salt or a customer group id as something an import may write. They are dropped out of the bindable catalog before a merchant is ever offered a mapping screen, so there is no field to map a column of a supplier's file to.

An importer that could write the password column is an importer that can take over every account in the store with a spreadsheet, and one that could write a salt is worse because nothing would notice. Filtering at the point the catalog is built rather than at the point a row is written is what makes it a refusal instead of a check: there is no screen on which the field appears, no stored mapping that can name it, and no code path that reaches it. The group id is dropped for a different reason — a group is bound by its name instead, so an import moved between stores puts people in the group they were meant to be in rather than in whichever one happens to hold that number.

The feed secret and the language-model provider key, in preflight_setting — the table this extension keeps a copy of its own configuration in so that an OpenCart upgrade does not wipe it.

That table is never dropped, by design, because the import history hangs off the same rule. A copy of a credential in it would outlive the extension: a store owner who removed Import/export entirely would still have their provider key sitting in their database with nothing left to read it. So the two are asked for again after an update instead, and until they are answered the feed and the generation are both off — which is the safe way round.

Where it goes

Wherever the merchant told this extension to read from: a URL they typed, fetched by this extension on their behalf. The request carries the URL and nothing else — no row of the store goes out with it — but the URL is the holding, because a merchant who pasted https://user:password@supplier.example/feed.csv has stored a credential in a column and sends it to that host on every run. It is their host, their credential and their decision; ours is to say that the column holds it.

Chosen by: merchant. What reaches it: preflight_job.source, preflight_profile.source.

The store's own mail transport, to the address in config_email, and only when a scheduled run needs a person — one that held a plan for review, or one that failed outright. A run that did what it was asked to do is logged and nothing else: an importer that emailed every morning is an importer nobody reads the email of. The message is the feed's name and the reason it stopped.

Chosen by: merchant. What reaches it: preflight_run.name, preflight_run.error.

The two language-model endpoints this extension can write catalog copy with — api.anthropic.com and api.openai.com, both fixed in our own source and reached only when the merchant has supplied their own key. Nothing about a person is sent to either, ever. The rule that makes it true is Entities::UNGENERATED, which refuses generation outright for the order, customer, review and coupon entities: not by default, not behind a setting, not with a warning. An instruction may name any field the same row maps — that is the feature — so a placeholder on a customer import would resolve to that person's address and on an order import to both addresses, the comment and the IP; a store owner cannot consent on behalf of the people in their file, so the four entities are refused at the point a job is checked rather than filtered later. Nothing is given up by it: generation writes a description, a meta title and a meta description, and not one of those four entities has any of them.

Chosen by: vendor. Nothing this extension holds about a person reaches it.

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:

  • preflight_label — Nothing on any clock removes a label. The retention purge deliberately leaves it: it is not rollback data, and a record whose provenance disappeared a month after it was imported would answer where did this come from with silence. What does replace one is the next job to write that same record, because the question is where the data came from and the last job to write it is the answer. The remove-everything screen is the only thing that takes it, which is what that screen is for.
  • preflight_job — The job row outlives its own undo and is meant to: uninstalling Import/export does not drop a table, a purge sets a flag rather than deleting a row, and the counts are how a store owner answers what did we import in March a year later. Nothing removes it on a clock, and the honest consequence — that the personal columns on it would otherwise sit there for ever with no screen able to reach them — is why the remove-everything screen exists and why it empties them rather than deleting the row.
  • preflight_profile — A saved feed is configuration and configuration has no expiry: a profile that vanished after thirty days would take a store's nightly import with it. It goes when the merchant deletes the feed, and not before. The remove-everything screen empties the personal columns on it and leaves the feed standing — which switches that feed off until its source is typed again, exactly as Configuration::SECRETS already leaves the provider key and the feed secret to be given again after an update, and for the same reason: the safe way round is off.

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, 117 — 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, 8 — 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, 8 — 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, 36 — 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, 35 — every holding the remove-everything screen reaches

What is left over

4 columns here are frozen copies of things the store already holds: preflight_plan.before_values as a whole customer or order record, preflight_plan.after_values as a whole customer or order record, preflight_journal.before_values as a whole customer or order record, preflight_journal.after_values as a whole customer or order record. 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 Import/export's own settings screen at Admin > Extensions > Extensions > Modules > Import/export. 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.