Skip to main content
You are reading an unvalidated pack: reference, not production, draft v0.1. Example data, not real prices.

Provenance

Questions: tattoo.co current constant, unvalidated Factors: tattoo.co current constant, unvalidated

Tattoo 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_5h uses value.additive_hours: 0.5; switching to multiplier_1_2 uses value.multiplier: 1.2 without resetting any weight. No default weight is set; an optional explicit weight overrides the selected mode’s default. Any tricky area applies the effect once. placement.value.match maps modes to matching behavior: additive_0_5h: substring (case-sensitive A) and multiplier_1_2: substring_ci (case-insensitive B).
  • complexity / style: asked or inferred_from_text.
  • wraparound: disabled by default; enabling wraparound_multiplier_1_3 applies weight 1.3 according to the pack inference rules below.
  • tier: fallback_standard or inferred. 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, or artist_set. Keep deposit_policy aligned with this factor’s mode. Formula B uses 50% of total, rounded to nearest 50. Factor value defines low_percentage: 0.3, total_percentage: 0.5, low_increment: 25, total_increment: 50, and piece_increment: 25, identically in the catalog and default model. Artist-set fixed deposits use the exact configured amount; none produces zero. Artist percentage deposits remain an open decision for Gavin: only an explicitly selected artist_set mode may use them, with explicit deposit_policy.basis: low | total | start and deposit_policy.rounding_increment: number | null. Missing parameters are an error; start requires the by-piece path. null means 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) sets review_status: MUTATION_REQUIRED, logs the skip, and asks for size in assumptions. The required price_range preserves the existing partial model calculation, including zero intermediate values; it is an unavailable service estimate, never a purchasable zero-price quote. Formula B’s fixed_base_hours mode uses size.value.unknown_base_hours: 4.
  • Top-level model keyword_match: substring by default for exact Formula B matching behavior; optional whole_word is 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 (portrait AND realistic), photorealistic, photo-realistic, (face AND realistic), or hyperrealism; otherwise detailed if detailed, intricate, complex, mandala, geometric, (realism AND (black OR grey)), or more than 3 reference images; otherwise moderate if shading, pattern, design, realistic, or description length >100 characters; otherwise simple.
  • Style: color if color, rainbow, blue, red, green, or colorful; otherwise popcolor if pop, accent, or highlight; otherwise blackgrey. Color takes priority over popcolor. US spelling only: colour is not a keyword. Substring false positives such as red in hundred are intentional source parity; businesses may opt into whole_word instead.
  • Tier: only when no artist rate exists: realism → senior; detailed, moderate, or color → standard; otherwise apprentice.
  • Wraparound: description contains wrap or around, or description contains sleeve and a placement contains arm or leg.

Pricing contract

Rates use currency, 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.

Questions

Factors

Full catalogs