Skip to content

Validation Rules

What values are valid for each field type.

How to read this doc: Rules marked Backend are enforced by Laravel FormRequest classes in upvendo-backend (cited as file:line). Rules marked UI-only are enforced only by a front-end validator in upvendo-backoffice and are not re-checked server-side. The backend is deliberately permissive: most item / category / location text fields are only required/nullable + string with no length limit, and most numbers are only numeric/integer with no min/max range unless listed below. A handful of branding and online-ordering text fields do carry a max — see "Fields that do have a max length".


Text Fields

Location Name

  • Backend: required|string — no min/max length.
  • Source: app/Http/Requests/BackOffice/Settings/Location/StoreLocationRequest.php:111 ('name' => ['required', 'string'])

Item Name

  • Backend: required|string — no min/max length, any characters.
  • Source: app/Http/Requests/BackOffice/Item/StoreItemRequest.php:87 ('name' => ['required', 'string'])

Category Name

  • Backend: required|string (nested under details).
  • Source: app/Http/Requests/BackOffice/Category/StoreCategoryRequest.php:42 ('details.name' => 'required|string')

Description (item / category / location)

  • Backend: nullable|string — optional, no max length.
  • Sources:
    • Item: app/Http/Requests/BackOffice/Item/StoreItemRequest.php:101 ('description' => 'nullable|string')
    • Category: app/Http/Requests/BackOffice/Category/StoreCategoryRequest.php:43 ('details.description' => 'nullable|string')
    • Location: app/Http/Requests/BackOffice/Settings/Location/StoreLocationRequest.php:120 ('description' => 'nullable|string')
  • Note: Loyalty program description is the exception — required|string (app/Http/Requests/BackOffice/Loyalty/StoreLoyaltyRequest.php:198).

Coupon / Offer Discount Code

  • Backend: <required-or-nullable>|string + unique-in-connection. No min length, no max length, no uppercase conversion. (nullable when the offer is saved as a draft, otherwise required.) Only present when the offer method is CODE.
  • Source: app/Http/Requests/BackOffice/Offer/StoreOfferRequest.php:55-65 ($rules['discount_code'] = [$necessity, 'string', new UniqueInConnectionWithModel(...)])
  • UI-only: back-office caps length at 10 characters. No uppercasing.
    • Source: upvendo-backoffice/src/views/marketing/offers/components/create-offers/OfferInformation.vue:50 ((v) => v?.length <= 10 || ...)

Fields that do have a max length

FieldRuleSource
slogannullable|string|max:100Settings/Branding/UpdateBrandingProfileRequest.php:46
short_descriptionnullable|string|max:150…/UpdateBrandingProfileRequest.php:47
wallet_program_namenullable|string|max:50…/UpdateBrandingProfileRequest.php:67
custom_domainnullable|string|max:255 (+ a closure check)StoreOnlineOrderingRequest.php:223
payment_methods.pos_cod_payment_codenullable|string|max:64StoreOnlineOrderingRequest.php:239
payment_methods.mplus_invoice_payment_codenullable|string|max:64StoreOnlineOrderingRequest.php:241
welcome_popup.messagenullable|string|max:1000StoreOnlineOrderingRequest.php:244

Advance Orders (lead time)

FieldRuleSource
lead_time_daysnullable|integer|min:0|max:365StoreItemRequest.php:146
available_pickup_daysnullable|arrayStoreItemRequest.php:147
available_pickup_days.*required + Rule::in(Carbon::getDays())StoreItemRequest.php:148

The same pair exists on the category request.

Device Profile — menu columns

FieldRuleSource
menu_item_columnsnullable|integer|in:2,3 (defaults to 2)StoreDeviceProfileRequest.php:211, default at :64

Number Fields

The backend does not define ranges for most numeric settings. The values below are the only numeric constraints found in FormRequest rules(). Fields from older revisions of this doc (prep time 1–180, delay range, idle-timeout default/max, delivery-radius default, tip default/array-count, loyalty points-per-currency / point-value, KDS timer ranges, volume range) have no matching backend rule and are not listed unless a real rule exists.

Order Capacity

All on StoreOnlineSettingsRequest:

FieldRule
order_capacity.time_slot_duration_minrequired|numeric|gt:0 (the back office restricts the choice to 5/10/15/20/30/45/60)
order_capacity.number_of_orders_per_time_slotnullable by default, upgraded to required|numeric|gt:0 when limit_orders_per_time_slot is set (:202)
order_capacity.number_of_items_per_time_slotsame pattern, keyed on limit_items_per_time_slot
order_capacity.value_threshold_per_time_slotnullable|numeric|gt:0
order_capacity.allow_up_tonullable|integer|gt:0
order_capacity.time_specific_rules.*.number_of_orders_per_time_slotnullable|numeric|gt:0

The request also normalises "enabled but empty/invalid" states before validating: a flexible-limit or category-limit toggle that is on but has no usable values is silently turned off rather than rejected.

Price / Item Pricing

  • Backend: required|numeric + MoneyFormat rule. No min, no max, no fixed decimal count.
  • The MoneyFormat rule switches on the X-Price-Format request header (falling back to config('upvendo.pricing_format'), default 'cents'):
    • cents → value must be an integer (cents).
    • anything else (legacy) → value must be numeric (float/decimal allowed).
  • Sources:
    • app/Http/Requests/BackOffice/Item/StoreItemRequest.php:116 ('price' => ['required', 'numeric', new MoneyFormat]), :125 ('pricing.*' => ['required', 'numeric', new MoneyFormat])
    • app/Rules/Money/MoneyFormat.php:15-29
    • config/upvendo.php:6 ('pricing_format' => env('PRICING_FORMAT', 'cents'))
  • Note: modifier prices are required|numeric only — no MoneyFormat. (app/Http/Requests/BackOffice/Modifier/StoreModifierGroupRequest.php:101)

Prep Time (item)

  • Backend: prep_time_seconds = nullable|numeric — no min/max, no default. (UI may send prep_time_minutes, which the request converts to seconds.)
  • Stored but not applied: item prep time is gated by ONLINE_ORDERING_INCLUDE_ITEM_PREP_TIME (config/upvendo.php:26, default false), which is not set in production, staging or testing.
  • Source: app/Http/Requests/BackOffice/Item/StoreItemRequest.php:141 ('prep_time_seconds' => 'nullable|numeric'); conversion at :48-54.

Average Prep Time (Location)

  • Backend: required|integer — no min/max. Defaults to 20 if omitted (set in prepareForValidation).
  • The 20 is now one shared constant, Constants::DEFAULT_LOCATION_AVERAGE_PREP_TIME_MINUTES (app/Constants.php:419), and every path that creates a location stamps it: the back-office form, the MplusKassa import (app/Services/BackOffice/MplusKassaIntegrationService.php:5825), the ShopCaisse import (app/Services/BackOffice/ShopCaisse/ShopCaisseLocationFactory.php:53) and the Square import (app/Services/BackOffice/Square/SquareLocationSync.php:269), which previously wrote no value at all.
  • It is not a read fallback. A location document that has no average_prep_time field reads back as 0, not 20 (app/RawFactories/LocationFactory.php:32) — so a location created before this was unified quotes a 0-minute prep until someone saves a value on it.
  • Source: app/Http/Requests/BackOffice/Settings/Location/StoreLocationRequest.php:137 ('average_prep_time' => 'required|integer'); default at :56-58.

Fulfillment Delay (takeout / delivery / eat-in)

  • Backend: required|numeric for takeout and delivery (only when that fulfillment type is enabled); nullable|numeric for eat-in. No min/max on any of them.
  • No min means negative values are accepted, and they are meant to be: a negative delay shortens the lead time for that channel. Only the finished prep figure is clamped, to 0-1440 minutes (app/RawModels/Location.php:2015-2017, ceiling at :60).
  • Source: app/Http/Requests/BackOffice/OnlineOrdering/StoreOnlineOrderingRequest.php:256,260 ('takeout.delay' => 'required|numeric', 'delivery.delay' => 'required|numeric') and :155 ('eat_in.delay' => 'nullable|numeric')

Idle Timeout

  • Backend: idle_timeout_seconds and show_warning_for_seconds = required_if:idle_timeout_type,custom|numeric|min:1. Min 1; no max, no default.
  • Source: app/Http/Requests/BackOffice/OnlineOrdering/StoreOnlineOrderingRequest.php:202-203

Scheduled Orders — Days in Advance

  • Backend: required|numeric|max:365 (per fulfillment type, when enabled — takeout, delivery and eat_in). Max 365; no min.
  • Source: app/Http/Requests/BackOffice/OnlineOrdering/StoreOnlineOrderingRequest.php:277,281,285 ('...days_in_advance' => 'required|numeric|max:365')

Scheduled Orders — Hours in Advance / Minutes Before Pickup

  • Backend: required|numeric|min:1 (when custom scheduled orders enabled). Min 1; no max.
  • Source: app/Http/Requests/BackOffice/OnlineOrdering/StoreOnlineOrderingRequest.php:251-252

Delivery Distance / Radius

  • Backend:
    • delivery_region.max_distance = required|numeric|min:1 (distance region type).
    • delivery_region.radius = required|numeric (other types) — no min/max.
  • Source: app/Http/Requests/BackOffice/OnlineOrdering/StoreOnlineOrderingRequest.php:292,294

Delivery Fee — Distance Pricing

  • Backend:
    • flat_rate_per_km = required|numeric|min:0.
    • distance_fees.*.max_km = required|numeric|gt:0; distance_fees.*.fee = required|numeric|min:0.
  • Source: app/Http/Requests/BackOffice/OnlineOrdering/StoreOnlineOrderingRequest.php:315,317-319

Minimum Order Amount

  • Backend: required|numeric (when enabled). No min/max.
  • Source: app/Http/Requests/BackOffice/OnlineOrdering/StoreOnlineOrderingRequest.php:325,329 ('minimum_order_amount.amount', 'pickup_minimum_order_amount.amount')

Tip Options (in-house dine-in)

  • Backend: each tip option = numeric|min:0|max:100 (when collect-tips enabled). Range 0–100; no array-count limit; no default values.
  • Source: app/Http/Requests/BackOffice/InHouseSettings/StoreInHouseSettingsRequest.php:60 ('collect_tips.options.*' => [$collectTipsNecessity, 'numeric', 'min:0', 'max:100'])

Loyalty — Points / Spend

  • Backend: points_earned_each_visit, points_earned_each_dollar, minimum_spend_to_earn_points, sign_up_bonus_points are all nullable|numeric (conditionally required). No min/max ranges.
  • Source: app/Http/Requests/BackOffice/Loyalty/StoreLoyaltyRequest.php:110-113,201
  • Reward discount_value (percentage) = required|numeric|gt:0|lte:100; (amount) = required|numeric|gt:0.
  • Source: app/Http/Requests/BackOffice/Loyalty/StoreLoyaltyRequest.php:179-181

KDS Timers / Preparation Time / Volume

  • Backend: required|integer for preparation_time_seconds, the for_here / to_go / pickup caution_time_seconds / late_time_seconds pairs, and sound.volume. No min/max ranges.
    • The timers.delivery bucket is the exception: it is sometimes|array and its caution_time_seconds / late_time_seconds are required_with:timers.delivery|integer, not required|integer.
    • preparation_time_seconds has no validation default. On KDS-profile create it is seeded from the location's average_prep_time x 60, falling back to Constants::DEFAULT_KDS_PREPARATION_TIME_SECONDS = 900 (15 min) when that is absent or zero (app/Services/KitchenDisplay/KdsProfileService.php:507-519, app/Constants.php:388). It is a seed, not a live link — later edits to the location do not move a stored profile.
  • Source: app/Http/Requests/KitchenDisplay/UpdateSettingsRequest.php:56,59-66,74-76,82

Offer Discount Value

  • Backend:
    • Percentage unit: required|numeric|gt:0|lte:100.
    • Amount unit: required|numeric|gt:0.
  • Source: app/Http/Requests/BackOffice/Offer/StoreOfferRequest.php:177-181

Offer Usage Limits

  • Backend: limit_discount_usage_amount, minimum_purchase_amount, minimum_quantity_items, benefit_quantity, max_usage_per_order are each numeric|gt:0 (when the enabling flag is set). No max.
  • Source: app/Http/Requests/BackOffice/Offer/StoreOfferRequest.php:163,172,174,185 (and :107 benefit_quantity)

Contact Fields

Email

  • Backend: Laravel email rule. nullable|email (customer) or required|email (location contact). No max length, no RFC-5322 claim.
  • Sources:
    • app/Http/Requests/BackOffice/Customer/StoreCustomerRequest.php:45 ('email' => 'nullable|email')
    • app/Http/Requests/BackOffice/Settings/Location/StoreLocationRequest.php:130 ('contact_information.email' => 'required|email')

Phone

  • Backend: regex against Constants::PHONE_REGEX = /^[+\d\s()-]+$/. This is a character-class check only — it allows digits, +, spaces, parentheses, and hyphens. No minimum/maximum digit count. Typically nullable|regex:... (customer) or required|regex:... (location contact).
  • Sources:
    • app/Constants.php:798 (public const PHONE_REGEX = "/^[+\d\s()-]+$/";)
    • app/Http/Requests/BackOffice/Customer/StoreCustomerRequest.php:46 ('phone' => 'nullable|regex:'.Constants::PHONE_REGEX)
    • app/Http/Requests/BackOffice/Settings/Location/StoreLocationRequest.php:131 ('contact_information.phone' => 'required|regex:'.Constants::PHONE_REGEX)

URL

  • No backend FormRequest rule found for a generic merchant-facing URL field. custom_domain (online ordering) is nullable|string|max:255 plus a subdomain-must-have-≥3-parts closure (not a URL/url rule).
    • Source: app/Http/Requests/BackOffice/OnlineOrdering/StoreOnlineOrderingRequest.php:223-232

Address Fields

Address rules differ slightly between the back-office customer address and the location address. Both are shown.

Customer Address

  • countryBackend required|string.
  • address (line 1) — Backend required|string.
  • cityBackend nullable|string (optional here).
  • postal_codeBackend nullable|string (optional).
  • state, company, unit_number, first_name, last_namenullable|string.
  • phonenullable|regex: Constants::PHONE_REGEX.
  • Source: app/Http/Requests/BackOffice/Customer/StoreCustomerAddressRequest.php:27-37

Location Address

  • address.line1Backend required|string.
  • address.cityBackend required|string.
  • address.countryBackend required|string.
  • address.postal_codeBackend nullable|string (optional).
  • address.line2nullable|string.
  • Source: app/Http/Requests/BackOffice/Settings/Location/StoreLocationRequest.php:122-126

No country-specific postal-code format validation exists in the backend. Postal code is accepted as any string. The per-country format table from older revisions of this doc is removed (no rule enforces it).

Country

  • Backend: required|string. No in: list, no ISO-3166 enforcement at the FormRequest level for these address fields.
  • Sources: customer :27, location :116 above.

Date & Time Fields

Date

  • Backend: date_format:Y-m-d (e.g. offer dates, restricted dates, date of birth).
  • Sources:
    • app/Http/Requests/BackOffice/Offer/StoreOfferRequest.php:197,202 (active_dates.start_date / end_date)
    • app/Http/Requests/BackOffice/Settings/Location/StoreLocationRequest.php:157,162,167 (restricted dates)
    • app/Http/Requests/BackOffice/Customer/StoreCustomerRequest.php:47 (date_of_birth)

Time

  • Backend: date_format:H:i — 24-hour HH:MM.
  • Sources:
    • Offer: app/Http/Requests/BackOffice/Offer/StoreOfferRequest.php:198,203
    • Loyalty bonus window: app/Http/Requests/BackOffice/Loyalty/StoreLoyaltyRequest.php:210-211
    • Order cutoffs: app/Http/Requests/BackOffice/OnlineOrdering/StoreOnlineOrderingRequest.php:158,161,164,167

Business Hours Time Range

  • Backend: each entry has from and to, both date_format:H:i (required when the day is_available, otherwise nullable).
  • Sources:
    • Location: app/Http/Requests/BackOffice/Settings/Location/StoreLocationRequest.php:185-186 (business_hours.{day}.times.*.from / .to)
    • Online ordering custom schedule: app/Http/Requests/BackOffice/OnlineOrdering/StoreOnlineOrderingRequest.php:271-272
  • Note: no backend rule enforces "end must be after start" for business-hours entries.

Restricted Date

  • Backend: date_format:Y-m-d. Date-range entries require date_until after_or_equal:date_from. temporarily_closed_until (online ordering) is date_format:Y-m-d|after_or_equal:today.
  • Sources: app/Http/Requests/BackOffice/Settings/Location/StoreLocationRequest.php:157-172; app/Http/Requests/BackOffice/OnlineOrdering/StoreOnlineOrderingRequest.php:234

Select / Enum Fields

Several fields are constrained to enum values via Rule::enum(...) rather than the literal lists in older revisions of this doc. The doc's standalone Country Codes / Language Codes / Currency Codes / Timezone lists are not enforced by these FormRequest rules and are omitted (no in: rule found for them in the address/contact requests).

Days of Week

  • Backend: Rule::in(Carbon::getDays()) (full day names: Sunday–Saturday).
  • Sources: offer active_days (StoreOfferRequest.php:196); online-ordering available days (StoreOnlineOrderingRequest.php:213,215); loyalty bonus days (StoreLoyaltyRequest.php:208).

Enum-Constrained Fields (examples)

  • Item status — Rule::enum(ItemStatuses::class) (StoreItemRequest.php:115).
  • Item / modifier platforms / visibility — Rule::enum(ChannelOptions::class) (StoreItemRequest.php:127, StoreModifierGroupRequest.php:108).
  • Modifier pricing — Rule::enum(PricingOptions::class) (StoreModifierGroupRequest.php:87).
  • Offer type / method / discount unit — Rule::enum(OfferTypes/OfferMethods/DiscountUnits::class) (StoreOfferRequest.php:36-37,69).
  • Image source — Rule::enum(ImageSources::class) (e.g. StoreItemRequest.php:102).

Array Fields

Business Hours

  • Backend: business_hours = required|array. Each day: is_available required|boolean, times array, each time { from, to } = date_format:H:i.
  • No "max 3 times per day" limit in the backend.
  • Source: app/Http/Requests/BackOffice/Settings/Location/StoreLocationRequest.php:128,175-186

Delivery Zone Postal Codes

  • Backend: delivery_region.postal_codes = required|array; each delivery_region.postal_codes.* = required|string.
  • No max-count limit (no "max 500").
  • Source: app/Http/Requests/BackOffice/OnlineOrdering/StoreOnlineOrderingRequest.php:289-290

Tip Options

  • Backend (in-house dine-in): collect_tips.options = array; each value numeric|min:0|max:100. No min/max item count.
  • Source: app/Http/Requests/BackOffice/InHouseSettings/StoreInHouseSettingsRequest.php:59-60

Allergens / Dietary Preferences (item)

  • Backend: each is nullable|array; entries required|string. No "must be from a predefined list" rule server-side (free-text strings).
  • Source: app/Http/Requests/BackOffice/Item/StoreItemRequest.php:133-136

Ingredients / Dietary Supplements (item)

  • Backend: nullable|array; entries required|string.
  • Source: app/Http/Requests/BackOffice/Item/StoreItemRequest.php:131-132,137-138

Image Fields

Item / Category / Modifier / Online-Ordering Landing Image

  • Backend: image.file = nullable|max:10240 (≈ 10 MB; max on an uploaded file is in kilobytes). image.source = nullable enum (ImageSources).
  • No mime/format restriction on these image.file fields (no mimes: rule).
  • Sources:
    • Item: app/Http/Requests/BackOffice/Item/StoreItemRequest.php:102-103 ('image.file' => 'nullable|max:10240')
    • Category: app/Http/Requests/BackOffice/Category/StoreCategoryRequest.php:45
    • Modifier: app/Http/Requests/BackOffice/Modifier/StoreModifierGroupRequest.php:103 ('modifiers.*.image.file' => 'nullable|max:10240')
    • Online ordering landing page: app/Http/Requests/BackOffice/OnlineOrdering/StoreOnlineOrderingRequest.php:211 ('landing_page_image.file', not image.file)

Recommended dimensions / min-dimension / per-image-type size differences (logo 2 MB, banner 10 MB, etc.) from older revisions are not enforced by any FormRequest and are removed. Where mime restrictions do exist they are on other uploads (e.g. guided-setup thumbnail mimes:jpg,jpeg,png,webp|max:5120, guided-setup video mimes:mp4,...|max:102400, app bundle mimes:zip).


Color Fields

  • All eight branding colour fields — primary_color, secondary_color, primary_text_color, secondary_text_color, primary_background_color, secondary_background_color, promo_code_font_color, promo_code_background_color — are required|string on update (Settings/Branding/UpdateBrandingProfileRequest.php:56-64). Required, but with no hex or format check.
  • The create request validates only name (Settings/Branding/StoreBrandingProfileRequest.php).
  • Any hex-format validation is therefore UI-only.

Special Validations

Modifier Group — Max Selected

  • Backend: settings.max_selected = required only (present when "allow select more than one" is enabled). No max:99 or numeric range. ("Allow select more than one", "is mandatory", and "allow same modifier more than once" are each required|boolean.)
  • Source: app/Http/Requests/BackOffice/Modifier/StoreModifierGroupRequest.php:104-106,119-121

Offer Discount Value Cap

  • Backend: percentage discounts are gt:0|lte:100; amount discounts gt:0.
  • Source: app/Http/Requests/BackOffice/Offer/StoreOfferRequest.php:177-181

Delivery Fee — Free Over Amount

  • Backend: when fee type is "free over certain amount", minimum_order_amount_for_free_delivery = required|numeric (no gt:0).
  • Source: app/Http/Requests/BackOffice/OnlineOrdering/StoreOnlineOrderingRequest.php:303

Offer Usage Limits

  • Backend: limit_discount_usage_amount = numeric|gt:0 (when limit_discount_usage is on). limit_one_usage_per_customer is a boolean flag (no per-customer numeric cap field).
  • Source: app/Http/Requests/BackOffice/Offer/StoreOfferRequest.php:183-188

Error Messages

Backend validation messages are Laravel defaults unless a request defines a messages() method (e.g. StoreLoyaltyRequest::messages(), app/Http/Requests/BackOffice/Loyalty/StoreLoyaltyRequest.php:259-294). Exact user-facing copy comes from the back-office i18n locale files, not from a fixed table — the strings below are illustrative, not authoritative.

ValidationTypical message
Required field empty"This field is required"
Invalid email"Please enter a valid email address"
Invalid phone"Please enter a valid phone number"
File too large"File size must be less than {max}"
Invalid time range"End time must be after start time"