If a quantity changes while an earlier confirmation remains visible, what exactly was confirmed? I developed a new revision-bound module to make the public ERP experience’s reset decision more explicit and reusable.

No client records or real approvers are included. This pure state model separates preparing a revision from confirming it. It does not implement server approval, electronic signatures or real ordering.

Confirmation needs a subject, not just a boolean

The state contains quantity, revision, preparedFor, confirmedFor and cancelled. The revision number increases when content changes. Preparation and confirmation stamps identify the revision they cover; null means the action has not happened. Progress is allowed only when those stamps belong to the current revision, preventing an earlier check from silently covering changed content.

Quantity is stored as text and must represent an integer from one to twenty before preparation. Preparing and confirming six, then editing it to seven, increments the revision and clears both stamps. Re-entering six without changing the content preserves the revision and its checks. That distinction separates a meaningful edit from the mere occurrence of an input event.

Invalid drafts and repeated actions are different cases

A blank string, zero, twenty-one or a fractional expression can remain a draft of at most twelve characters, but cannot be prepared. The screen preserves the input instead of erasing the thing that needs correction. Longer text is rejected by the transition function. The UI and the model both have the length boundary, including for callers that bypass the input element.

Confirmation before preparation, or confirmation repeated after it is already complete, does not create more progress. Returning the same state makes harmless repetition within a page easy to inspect. I do not call this network request idempotency. A real server still needs request keys, transactions and concurrency control. The module deliberately separates a local transition contract from those operational responsibilities.

Give cancellation an explicit path back to a new example

Cancellation clears preparation and confirmation, then blocks content edits and progression. Pressing another action does not quietly reactivate the example. Starting again creates an initial state through an explicit reset. A business service may need cancellation permissions, reasons and reversal documents; this module does not implement those policies. It makes the narrower boundary after cancellation explicit.

In the implementation below, workflowView derives permitted actions and transitionWorkflow returns the next state. It does not mutate the old state, making a given input and action straightforward to test. The demo’s disabled buttons also use that same view. This reduces the opportunity for a model to permit an action that the UI treats as forbidden, or the reverse.

Explore the implementation

Complete independent synthetic workflow model

// Independent synthetic preparation/confirmation model. No production writes.
const keys = 'cancelled,confirmedFor,preparedFor,quantity,revision';
const validQuantity = q => typeof q === 'string' && /^(?:[1-9]|1\d|20)$/.test(q);
export function initialWorkflow() {
  return {quantity:'6', revision:0, preparedFor:null, confirmedFor:null, cancelled:false};
}
export function validateWorkflow(s) {
  if (!s || typeof s !== 'object' || Array.isArray(s) || Object.keys(s).sort().join(',') !== keys) return false;
  if (typeof s.quantity !== 'string' || s.quantity.length > 12 || !Number.isSafeInteger(s.revision) || s.revision < 0 || typeof s.cancelled !== 'boolean') return false;
  for (const key of ['preparedFor','confirmedFor']) if (s[key] !== null && s[key] !== s.revision) return false;
  if (s.confirmedFor !== null && s.preparedFor !== s.revision) return false;
  if ((s.preparedFor !== null || s.confirmedFor !== null) && (!validQuantity(s.quantity) || s.cancelled)) return false;
  return true;
}
export function workflowView(s) {
  if (!validateWorkflow(s)) throw new TypeError('Invalid workflow state');
  const valid = validQuantity(s.quantity);
  return {valid, canPrepare:valid && !s.cancelled && s.preparedFor === null,
    canConfirm:valid && !s.cancelled && s.preparedFor === s.revision && s.confirmedFor === null,
    state:s.cancelled ? 'cancelled' : !valid ? 'invalid_quantity' : s.confirmedFor !== null ? 'confirmed' : s.preparedFor !== null ? 'prepared' : 'draft'};
}
export function transitionWorkflow(s, action) {
  if (!validateWorkflow(s)) throw new TypeError('Invalid workflow state');
  if (!action || typeof action !== 'object') throw new TypeError('Invalid action');
  if (action.type === 'reset') return initialWorkflow();
  if (!['edit','prepare','confirm','cancel'].includes(action.type)) throw new TypeError('Unknown action');
  if (s.cancelled) return s;
  if (action.type === 'edit') {
    if (typeof action.quantity !== 'string' || action.quantity.length > 12) throw new TypeError('Draft text too long');
    if (action.quantity === s.quantity) return s;
    if (!Number.isSafeInteger(s.revision + 1)) throw new RangeError('Revision exhausted');
    return {...s, quantity:action.quantity, revision:s.revision+1, preparedFor:null, confirmedFor:null};
  }
  if (action.type === 'cancel') return {...s, cancelled:true, preparedFor:null, confirmedFor:null};
  const v = workflowView(s);
  if (action.type === 'prepare' && v.canPrepare) return {...s, preparedFor:s.revision};
  if (action.type === 'confirm' && v.canConfirm) return {...s, confirmedFor:s.revision};
  return s;
}
export function snapshotWorkflow(s) {
  if (!validateWorkflow(s)) throw new TypeError('Invalid workflow state');
  return JSON.stringify({format:'synthetic-review-v1', synthetic:true, workflow:s});
}
export function restoreWorkflow(text) {
  if (typeof text !== 'string' || text.length > 1200) throw new TypeError('Invalid snapshot text');
  const p = JSON.parse(text);
  if (!p || typeof p !== 'object' || Object.keys(p).sort().join(',') !== 'format,synthetic,workflow' || p.format !== 'synthetic-review-v1' || p.synthetic !== true || !validateWorkflow(p.workflow)) throw new TypeError('Unsupported synthetic snapshot');
  return {...p.workflow};
}

Restored state must satisfy the same contract

The snapshot string carries a format version and an explicit synthetic marker. Restoration checks a 1,200-character limit, exact known keys, a safe revision number and the preparation-confirmation relationship. Stamps for another revision, confirmation without preparation and confirmation retained in a cancelled state are rejected. Unknown fields are not copied forward into later logic.

An invalid draft may be restored as a draft, but it cannot carry preparation or confirmation stamps. The memory-copy button retains a snapshot only for the life of the current page. It can return to an example copied before cancellation, so it does not protect a real cancellation record. Restoring it is neither a signed approval record nor a durable backup. A database integration needs actor authorization and server-side version conflict checks in addition to shape validation.

Try the relationship in a small closed example

Prepare and confirm a quantity in the demo, then change it. Both stamps disappear and preparation is required again. Try a blank quantity, cancellation, reset and memory copy and restore as separate actions. The page makes no external requests and accepts no company records. Reloading removes its memory copy. Both language screens inline the same implementation, so translating the labels does not create different workflow rules.

The ZIP includes the complete model, tests, standalone Korean and English HTML demos, documented boundaries and reuse permission. Extract it and run node test.mjs before deciding how it should connect to another system. Modification, redistribution and commercial reuse are permitted without warranty. The files contain no operational client source or real approval material.

Synthetic state; page memory only; no real orders

Keep the verified contract separate from production responsibilities

I ran 25 test groups from the extracted package. Across quantities one to twenty and 4,320 short combinations of preparation, confirmation, cancellation, reset, invalid drafts and changed drafts, I checked valid state and snapshot round-trips. Separate checks reject future or stale stamps, extra fields, unsafe revision numbers and unsupported formats. These results describe this synthetic contract.

The bilingual demo wiring was also evaluated in code. Browser layout, focus behaviour and assistive-technology reading were not tested in this round. For a company deployment, first decide which fields require confirmation again. Then design identity, approvers, concurrent editing, storage, external sending and retries as separate responsibilities. This module exposes the subject of a check; it is not a finished approval system that conceals those decisions.

Download code, tests and bilingual demos
Independent synthetic example; no client records