Recued
Menu
← Back to recipes

Price and send a quote

by recued-core v3 10 views

Use “Price and send a quote” in Recued. Pin your price onto a waiting quote request, then send the customer a payment link for exactly that figure.

Complete workflow

Seller Quote Request 5 pinned recipes

Use Seller Quote Request in Recued for unspecified pricing, estimate, order, and reception. It includes 7 built-in actions and 4 ready-to-run workflows. Actions that change data use Recued's approval controls.

You are viewing one recipe in this workflow. Installing it opens the complete pack so its other recipes and background automations arrive together.

Use “Price and send a quote” in Recued. Pin your price onto a waiting quote request, then send the customer a payment link for exactly that figure.

pack:seller-quote-requestsellerquoteunspecified-pricing
by recued-core pinned v3

Use “Open a quote request” in Recued. Exact-form template that turns one owner-accepted Reception intake response into an order awaiting your price, and records the request itself in the pack's own Records storage. Changes remain behind Recued's approval controls.

pack:seller-quote-requestform-response-templatesellerquote
by recued-core pinned v3

Use “Observe a quote payment” in Recued. It carries out the named workflow using the connections and settings you choose.

pack:seller-quote-requestsellerquotepayment
by recued-core pinned v1

Use “Quote board” in Recued. Every quote request against one offer, with the two things you act on: which ones are still waiting for your price, and which ones you have priced and are waiting on the customer. It reads information without changing data in the connected service.

pack:seller-quote-requestsellerquoteboard
by recued-core pinned v3
View pack details, dependencies, and permissions →

Pricing a request

Run it with the offer and the submission reference the quote-request card printed, plus your price.

Amount is in minor units — pence, cents — so £250.00 is 25000. It must be a whole positive number; the recipe refuses a fraction rather than rounding one, because rounding somebody's invoice silently is not a thing software should do.

What happens, in order: the order is re-derived and confirmed to still be waiting for a price; core.seller.order.quote pins the total under a revision check; a Stripe Checkout Session is created for the total the server pinned (read back off the order, not off your input); the session is bound to the order with core.seller.order.attach-payment, which moves it to awaiting_payment; the session is re-read fresh from Stripe; and only then does the customer get an email with the figure and the link.

Every one of those writes is approval-gated, so you confirm the send before it leaves. That gate IS the approval step in this flow — there is no separate consent link to mint.

Re-running RESUMES. The run is phase-convergent: if the price is already pinned at the same figure it skips the quote, if a checkout session is already attached it reuses that session rather than minting a second one, and the mail's reconciliation id means an already-sent link is not sent twice. So a crash anywhere after the quote — before the session, after the session but before the bind, or at the send — is recovered by running it again with the same inputs.

A different price is refused. Once a figure is pinned the customer may already have seen it, so re-running with another amount stops before touching anything. To change a price, cancel the order and take a fresh request.

The Stripe idempotency key comes from the order, never composed here. That is what makes a crash between the Stripe call and the bind safe to retry: re-attaching the same session converges, and attaching a different one is refused as a double charge.

⚠ A retry must ask for the SAME price AND the same currency. An order already quoted at one currency cannot be re-quoted into another by re-running with a different currency — the run refuses rather than charging in the committed currency.

The checkout expiry is anchored to the quote, not to the clock. A Stripe idempotency key may only be reused with the SAME parameters, and the key here is stable per order — so if the expiry moved with wall-clock time, a run that died between creating the session and attaching it could never be retried. Deriving the expiry from the order's own quote timestamp makes every attempt byte-identical.

The trade is that the window ages. Stripe requires an expiry at least 30 minutes ahead, so if you retry long after quoting, the run refuses with a stale-quote guard rather than silently shifting the parameter. Cancel the order and take a fresh quote — the price the customer was shown has expired anyway.

Pricing refuses a request it cannot already find under this offer. order.open is deterministic in (offer, submission) but it CREATES on a miss, so a run pointed at the wrong offer used to mint a second order for the submission and only then stop — and a repeat of the same wrong run found that order waiting and priced it. The check is now a read that happens before anything is written: if Open a quote request has not run for this submission under THIS offer, pricing refuses and nothing is created.

What it writes back to the request record

After the mail is reconciled, the run records the subject and covering note it just emailed, and a quoted_at stamp, onto the pack's Records row for this request. That is what lets the board show what a quote was for months later, when the intake response is long out of the recent window.

Three things about that write are deliberate:

  • It happens last. A failure there cannot affect the price, the session, or the email — all of which have already converged — and re-running recovers it.
  • It happens once. If the row already carries a quoted_at, the run leaves it alone. The row records what was actually emailed; editing the note afterwards and re-running must not rewrite history into something the customer never saw. It also keeps a converged rerun quiet — no revision churn, and no change event for a change that did not happen.
  • quoted_at is anchored to the order's own quote stamp, the same anchor the checkout expiry uses, so every attempt at the same run writes the same instant.

A missing request row does not stop anything. An order opened before this pack gained Records storage has no row to write to; the run prices and sends exactly as before and simply records nothing. The board shows a blank For for those, which is what it did for every row previously.

Nothing about money is copied onto the row. No total, no currency, no phase, no paid flag. The seller order store answers those, and a second copy that could disagree with it would be worse than no copy at all.

How it works 107 steps

Inspect the data fetches, transforms, gates, and output this recipe runs.

Data fetch
form_response ?
Process (106 steps)
offer_id trim
Trim whitespace from setting seller offer id
offer_id_lowercase lowercase
Convert offer id to lowercase
offer_id_is_lowercase compare
Check if offer id equals offer id lowercase
submission_ref trim
Trim whitespace from setting submission ref
amount_number to_number
Convert setting amount minor to a number
amount_floor math
Calculate amount number floor
amount_is_whole compare
Check if amount number equals amount floor
amount_positive compare
Check if amount number is greater than 0
amount_bounded compare
Check if amount number is at most 99999999
currency_trimmed trim
Trim whitespace from setting currency
currency_input lowercase
Convert currency trimmed to lowercase
currency_chars split
Split currency input into parts
currency_length count
Count items in currency chars
currency_is_three compare
Check if currency length equals 3
visitor_email_raw default
Apply default
visitor_email_trimmed trim
Trim whitespace from visitor email raw
visitor_email lowercase
Convert visitor email trimmed to lowercase
visitor_email_present compare
Check if visitor email has data
visitor_email_has_at compare
Check if visitor email contains @
sender_mail trim
Trim whitespace from setting sender mail instance
quote_subject_value trim
Trim whitespace from setting quote subject
quote_subject_bounded_value truncate
Truncate quote subject value to a max length
quote_subject_bounded compare
Check if quote subject value equals quote subject bounded value
quote_note_value trim
Trim whitespace from setting quote note
success_url trim
Trim whitespace from setting checkout success url
success_url_https starts_with
Check if success url starts with https://
cancel_url trim
Trim whitespace from setting checkout cancel url
cancel_url_https starts_with
Check if cancel url starts with https://
expiry_hours to_number
Convert setting checkout expiry hours to a number
expiry_at_least_half_hour compare
Check if expiry hours is at least 0.5
expiry_at_most_a_day compare
Check if expiry hours is at most 24
expiry_seconds_raw math
Calculate expiry hours times 3600
expiry_seconds math
Calculate expiry seconds raw floor
expiry_is_whole_seconds compare
Check if expiry seconds raw equals expiry seconds
inputs_valid all
Check if all conditions are true
inputs_guard guard
Stop if inputs valid equals
offer_read ?
offer_valid all
Check if all conditions are true
offer_guard guard
Stop if offer valid equals
request_lookup ?
request_rows default
Apply default
request_count count
Count items in request rows
request_found compare
Check if request count is greater than 0
defaults defaults
Apply defaults
request_already_quoted compare
Check if request quoted at has data
recorded_contact_name default
Apply default
greeting_name trim
Trim whitespace from recorded contact name
has_greeting_name compare
Check if greeting name has data
greeting_line ternary
Apply ternary
existing_orders ?
existing_order_rows default
Apply default
existing_order_count count
Count items in existing order rows
request_already_opened compare
Check if existing order count is greater than 0
unopened_request_guard guard
Stop if request already opened equals
order_open ?
defaults_2 defaults
Apply defaults
order_pre_existed compare
Check if order open result does not equal created
order_quotable compare
Check if order open order phase is one of draft,pricing,awaiting_payment
order_amount_now default
Apply default
order_currency_now lowercase
Convert order open order currency to lowercase
needs_quote compare
Check if order amount now is empty
amount_already_matches compare
Check if order amount now equals amount number
currency_already_matches compare
Check if order currency now equals currency input
same_price_as_before all
Check if all conditions are true
order_ready_to_price all
Check if all conditions are true
order_guard guard
Stop if order ready to price equals
order_quote ?
skip: step.needs_quote equal false
effective_amount coalesce
Use the first available value from: order quote order amount minor, order open order amount minor
effective_currency coalesce
Use the first available value from: order quote order currency, order open order currency
effective_revision coalesce
Use the first available value from: order quote order revision, order open order revision
defaults_3 defaults
Apply defaults
quoted_currency_lower lowercase
Convert quoted currency to lowercase
quote_landed all
Check if all conditions are true
quote_guard guard
Stop if quote landed equals
now_iso date_add
Add 0 seconds to
now_ms date_parse
Parse now iso as a date
expiry_window_ms math
Calculate expiry seconds times 1000
quote_anchor_ms coalesce
Use the first available value from: order quote order updated at, order open order updated at
checkout_expires_at_ms math
Calculate quote anchor ms plus expiry window ms
checkout_expires_at_seconds_raw math
Calculate checkout expires at ms divided by 1000
checkout_expires_at_seconds math
Calculate checkout expires at seconds raw floor
expiry_floor_ms math
Calculate now ms plus 1800000
expiry_still_ahead compare
Check if checkout expires at ms is greater than expiry floor ms
attached_session_id default
Apply default
needs_session compare
Check if attached session id is empty
stale_quote_guard guard
Stop if expiry still ahead equals
skip: step.needs_session equal false
checkout_currency_lower lowercase
Convert quoted currency to lowercase
product_name default
Apply default
checkout_session_create ?
skip: step.needs_session equal false
session_id coalesce
Use the first available value from: checkout session create result id, attached session id
session_url coalesce
Use the first available value from: checkout session create result url, order open order checkout url
session_created all
Check if all conditions are true
session_guard guard
Stop if session created equals
attach_payment ?
skip: step.needs_session equal false
awaiting_payment any
Check if any of the conditions are true
attach_guard guard
Stop if awaiting payment equals
session_reread ?
reread_matches_order all
Check if all conditions are true
reread_guard guard
Stop if reread matches order equals
quote_reconciliation template
Generate text from a template
quote_mail_send ?
quote_mail_reconcile ?
quoted_at date_add
Add 0 seconds to
record_quote_on_request all
Check if all conditions are true
request_update ?
skip: step.record_quote_on_request equal false
card to_summary
Format results as a summary card
Settings 11 configurable

Configurable at install. Defaults shown — change them anytime in Recued.

stripe setting =
currency setting = gbp
quote note setting = [object Object]
amount minor setting = 0
quote subject setting = [object Object]
submission ref setting = [object Object]
seller offer id setting = [object Object]
checkout cancel url setting =
checkout success url setting =
sender mail instance setting = [object Object]
checkout expiry hours setting = 24

Trust & control

What installing this recipe would let it do. Recued grants these permissions at install — review them there before approving.

Data it touches

Warehousedata.maildata.form

About

Tags

pack:seller-quote-requestsellerquoteunspecified-pricingordercheckoutstripemoneyrecordsv3

Details

107 steps 11 configurable settings recipe_id: price-and-send-quote