Provenance
Questions: tattoo.co current constant, unvalidated Factors: tattoo.co current constant, unvalidatedTattoo pack v0
Pack id:tattoo; version: 0.1.0. These are synthetic reference settings,
not validated market prices or real quote data. The pricing and intake constants
are attributed by the authorized handoff and PRD to tattoo.co f018b86.
The source handoff file is absent from this workspace; the tattoo.co source was
not independently inspected. Existing constants are marked
"provenance": "tattoo.co current constant, unvalidated" on each catalog,
model, question, numeric option, and factor. The model’s top-level provenance
applies to its entire numeric maps. Catalog numeric maps inherit their containing
factor’s provenance; rates and minimums also carry it explicitly.
Formula B inference rules in inference-rules.json carry
"provenance": "tattoo.co f018b86 server/pricing-calculator.ts, unvalidated".
These source-parity claims follow PRD-003, not independent source verification.
Exception: expiry_days: 7 is an illustrative, configurable business setting,
not a tattoo.co source constant. Pack version and this expiry are UQP metadata.
No fixed validity default is imposed on businesses.
Inputs and switches
questions.json preserves size exactly as xs|sm|md|lg|xl|unsure. Optional
positive width and height in inches override square defaults only when both
are provided. placement is a multi-select of the 16 listed areas. Description,
budget bounds, and first-tattoo status must contain no contact details. Budget
and first-tattoo answers are informational: they have no pricing factor and
cannot change the result. Consent is explicit evaluator/request metadata, not
an intake question; no contact or consent question is added here.
reference_images is a raw numeric count only, not image data or URLs.
Questions map to factors through questions.json → factor_ids[]. A question
is omitted only when all its consuming factors are disabled; informational
questions without factors remain available. Inferred complexity/style requires
description rather than the corresponding selection question. A disabled factor
contributes its identity (×1 or +0) and logs
{step, factor_id, skipped: true, reason: "disabled"}. Answers to disabled
factors are ignored for pricing but preserved in raw answers; they cannot
re-enable a factor. The placement question maps to both placement and
wraparound, so disabling placement alone does not remove it when wraparound
needs it.
default-pricing-model.json selects Formula A. Formula B is available only
through per-business switches, not as the default model. Factor ids are size,
placement, complexity, style, wraparound, tier, deposit, sessions,
cover_up, multi_area, city_rate, and non_usd.
placement:additive_0_5husesvalue.additive_hours: 0.5; switching tomultiplier_1_2usesvalue.multiplier: 1.2without resetting any weight. No default weight is set; an optional explicitweightoverrides the selected mode’s default. Any tricky area applies the effect once.placement.value.matchmaps modes to matching behavior:additive_0_5h: substring(case-sensitive A) andmultiplier_1_2: substring_ci(case-insensitive B).complexity/style:askedorinferred_from_text.wraparound: disabled by default; enablingwraparound_multiplier_1_3applies weight1.3according to the pack inference rules below.tier:fallback_standardorinferred. Explicit artist rate wins. Inferred realism uses senior; detailed, moderate, or color uses standard; otherwise apprentice.deposit:pct_of_low_30,pct_of_total_50, orartist_set. Keepdeposit_policyaligned with this factor’s mode. Formula B uses 50% of total, rounded to nearest 50. Factorvaluedefineslow_percentage: 0.3,total_percentage: 0.5,low_increment: 25,total_increment: 50, andpiece_increment: 25, identically in the catalog and default model. Artist-set fixed deposits use the exact configured amount;noneproduces zero. Artist percentage deposits remain an open decision for Gavin: only an explicitly selectedartist_setmode may use them, with explicitdeposit_policy.basis: low | total | startanddeposit_policy.rounding_increment: number | null. Missing parameters are an error;startrequires the by-piece path.nullmeans no rounding. No basis or rounding default is chosen or shipped for artist percentages; this mode is off in the default pricing model.size.mode:no_estimate(Formula A default) setsreview_status: MUTATION_REQUIRED, logs the skip, and asks for size in assumptions. The requiredprice_rangepreserves the existing partial model calculation, including zero intermediate values; it is an unavailable service estimate, never a purchasable zero-price quote. Formula B’sfixed_base_hoursmode usessize.value.unknown_base_hours: 4.- Top-level model
keyword_match:substringby default for exact Formula B matching behavior; optionalwhole_wordis a per-business stricter alternative.
Formula B inference data
inference-rules.json stores ordered, data-only rules with ids, target factors,
output values, conditions, and provenance. No regex or code evaluation is used.
Matching is case-insensitive substring by default; first matching rule wins.
PRD-003 attributes these rules to server/pricing-calculator.ts at f018b86
(unvalidated). Inferred modes ignore the client’s selected complexity/style.
- Complexity: realism if (
portraitANDrealistic),photorealistic,photo-realistic, (faceANDrealistic), orhyperrealism; otherwise detailed ifdetailed,intricate,complex,mandala,geometric, (realismAND (blackORgrey)), or more than 3 reference images; otherwise moderate ifshading,pattern,design,realistic, or description length >100 characters; otherwise simple. - Style: color if
color,rainbow,blue,red,green, orcolorful; otherwise popcolor ifpop,accent, orhighlight; otherwise blackgrey. Color takes priority over popcolor. US spelling only:colouris not a keyword. Substring false positives such asredinhundredare intentional source parity; businesses may opt intowhole_wordinstead. - Tier: only when no artist rate exists: realism → senior; detailed, moderate, or color → standard; otherwise apprentice.
- Wraparound: description contains
wraporaround, or description containssleeveand a placement containsarmorleg.
Pricing contract
Rates usecurrency, pricing_mode, tiers, range_band, and
piece_high_multiplier. Optional rates.artist contains hourly_rate,
shop_minimum, piece_starting_price, deposit_type, deposit_amount, and
deposit_percentage (a fraction, like the model’s 0.3). by_piece uses
its piece path only when a piece starting price is set. Without one it falls
back to hourly and logs the reason; it does not throw. both returns only the
hourly range and logs pricing_mode: both → hourly.
Hourly base hours are max(1, ceil(width * height / 12)). Apply placement,
then complexity, then multiply hours by rate and style (Formula A).
Formula B instead uses base × complexity, then tricky placement ×1.2, then
wraparound ×1.3, then rate × style. The shop minimum is
max(subtotal, artist.shop_minimum ?? rate). Apply the 0.85/1.15 band and
round each bound to the nearest 50. Formula A deposit rounds 30% of low to
nearest 25. Preserve full-precision hours for money calculations; display hours
to one decimal: Formula A uses Math.round(t * 10) / 10, while Formula B
uses toFixed(1). These can differ at binary .x5 edges; log both display
values alongside unrounded hours, retaining B’s toFixed(1) string. The default
shown value uses A’s Math.round(t * 10) / 10. Rounding
hours before pricing would not reliably preserve the supplied golden ranges.
by_piece bypasses hourly math and the band: low is the starting price, high
is nearest-50 rounding of start times 1.5, and both formulas use deposit
max(100, round25(start * 0.3)). Hours are zero. A disabled factor is logged
as skipped and contributes the identity, never its configured weight/value.
Sessions/session caps, cover-up, multi-area summing, city rate, and non-USD
currency are explicitly not_yet_modeled, disabled, and contain no pricing
math. Multiple placements do not add areas. A provisional session envelope
uses one session with the estimated hours for hourly work, or zero sessions
and zero hours for by-piece; this is not session splitting or a session cap.
The ordered intermediates log records resolved factor inputs (asked/inferred,
matched rule id and keywords), base hours, placement adjustment, complexity,
wraparound, unrounded hours, displayed hours, rate and source
(from_artist | tier:<name>), style, subtotal, whether the shop minimum applied,
total, band bounds before/after rounding, deposit basis, raw deposit, rounded
deposit, and any by-piece fallback reason. Both source display-hour values are
logged without rounding money inputs. Preserve raw answers without PII.
Platform fees and BNPL stay out of the Quote entirely, including assumptions.
Evaluator API
evaluate(answers, pricingModel, now) accepts the question-answer map plus
explicit consent metadata. For a QuoteRequest, pass
{ ...request.answers, consent: request.consent }; now is an explicit Date or
RFC 3339 timestamp. The result separates consent from the preserved raw answers.
getEnabledQuestions(pricingModel) returns the intake question ids to display.
buildQuoteManifest(model, pricingModelRef) constructs a QuoteManifest using
that same enabled-question set.
The evaluator performs no I/O, calls no clock, and does not mutate its inputs.
A pre_price result remains pending artist review with a required non-binding range;
intermediates, sessions, and deposit are explicitly provisional. post_price
also returns the computed range for review. No artist approval is implied. Consent
preferences are copied, but anonymized remains false: preserving free text
is not proof of deidentification, so callers must not forward it as anonymous
research data without a separate privacy step.
Estimate and binding Quote lifecycle
evaluate() returns kind: estimate, binding: false, with a required
price_range (min, max, currency, basis, method, provenance)
and low confidence for these unvalidated example rules. Range and deposit
amounts both include provenance (who, when). Deposit policies come from
deposit_policy.policies when configured; otherwise they are explicitly
example placeholders requiring provider review. Example data, not real prices.
Artist review produces a binding Quote in QUOTED with valid_until.
Acceptance requires explicit approval of the amount and nonempty policies
before payment handoff. Provider is merchant of record; UQP never holds funds.
A deposit is paid only after trusted payment-processor confirmation, never
client self-report. Poll instead of holding calls open. Webhook delivery is deferred in the local reference bindings.