Recued
Menu
← Back to recipes

Price and send a quote

by recued-core v6 44 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 v6

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 “Open a quote request by hand” in Recued. Open a quote request you took by phone, in person, or in an email thread — an order waiting for your price, plus the request record Price and send a quote reads the customer's address from. Changes remain behind Recued's approval controls.

pack:seller-quote-requestsellerquoteunspecified-pricing
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 v4
View pack details, dependencies, and permissions →

Pricing a request

Run it with the offer and the request reference the quote-request card printed, plus your price. For a form request that reference is the submission id; for one opened with Open a quote request by hand it is manual- followed by your own reference. The board's Price it button carries both.

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.

When you run it yourself, the order and Stripe writes go through without a prompt — your own runs may write — and the run stops at the email: you approve the send before the customer gets anything. 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 — or Open a quote request by hand for this reference — pricing refuses and nothing is created.

Who the quote is sent to

For a form request the address comes from the accepted response itself, never from the request record or the payment page. A request opened by hand has no response, so its address is the one typed when it was opened, read from the request record. That makes the record load-bearing for those requests only: pricing one without it refuses at recipient_guard, before anything is written, rather than guessing an address.

The two are told apart by the reference. Only a hand-opened request contains manual-, because a submission id is a UUID, which never does. The order store has the last word: the kind is passed to the order.list pre-read, so a reference that claims the wrong kind finds no order and is refused as unopened.

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.

⚠ For a form request, 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 111 steps

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

Data fetch
▼ form_response core.data.form-response.get
Get data form response
skip: config.submission_ref contains manual-
▼
Process (110 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
■ is_manual compare
Check if submission ref contains manual-
■ origin_kind ternary
Apply ternary
■ 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
■ 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 core.seller.offer.get
Get seller offer
■ offer_valid all
Check if all conditions are true
⚠ offer_guard guard
Stop if offer valid equals
◆ request_lookup recued-core.seller-quote-request.quote_request.search
Search quote request on seller quote request
■ 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
■ form_email_raw default
Apply default
■ recorded_email_raw default
Apply default
■ visitor_email_raw ternary
Apply ternary
■ visitor_email_trimmed trim
Trim whitespace from visitor email raw
■ visitor_email lowercase
Convert visitor email trimmed to lowercase
■ recipient_valid all
Check if all conditions are true
⚠ recipient_guard guard
Stop if recipient valid equals
■ 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 core.seller.order.list
List seller order
■ 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 core.seller.order.open
Open seller order
■ 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 core.seller.order.quote
Quote seller order
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 recued-core.seller-stripe.checkout.session.create
Create checkout session on seller stripe
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 core.seller.order.attach-payment
Attach payment seller order
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 recued-core.seller-stripe.checkout.session.read
Read checkout session on seller stripe
■ 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 core.mail.send
Send mail
◆ quote_mail_reconcile core.mail.sent.reconcile
Reconcile mail sent
■ quoted_at date_add
Add 0 seconds to
■ record_quote_on_request all
Check if all conditions are true
◆ request_update recued-core.seller-quote-request.quote_request.update
Update quote request on seller quote request
skip: step.record_quote_on_request equal false
■ card to_summary
Format results as a summary card
▼
Output
summary card
Settings 10 configurable

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

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

Uses (12 operations)

Form responses Get data form response core.data.form-response.get
Selling Get seller offer core.seller.offer.get
Pack operation Search quote request on seller quote request recued-core.seller-quote-request.quote_request.search
Selling List seller order core.seller.order.list
Selling Open seller order core.seller.order.open
Selling Quote seller order core.seller.order.quote
Pack operation Create checkout session on seller stripe recued-core.seller-stripe.checkout.session.create
Selling Attach payment seller order core.seller.order.attach-payment
Pack operation Read checkout session on seller stripe recued-core.seller-stripe.checkout.session.read
Mail Send mail core.mail.send
Mail Reconcile mail sent core.mail.sent.reconcile
Pack operation Update quote request on seller quote request recued-core.seller-quote-request.quote_request.update

Trust & control

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

What it's allowed to do

Read Form responses core.data.form-response.get read-only
Read Selling core.seller.offer.get read-only
Write Pack Seller Quote Request external change
Read: quote_request.search · No approvalWrite: quote_request.update · No approval
Read Selling core.seller.order.list read-only
Write Selling core.seller.order.open external change
Write Selling core.seller.order.quote external change
Write Selling core.seller.order.attach-payment external change
Write Mail core.mail.send external change
Write Mail core.mail.sent.reconcile external change

2 more aren't listed here. You'll see them at install.

Data it touches

SurfacesForm responsesSellingMail
Warehousedata.maildata.form

About

Tags

pack:seller-quote-requestsellerquoteunspecified-pricingordercheckoutstripemoneyrecordsv3

Details

111 steps 10 configurable settings recipe_id: price-and-send-quote