Skip to main content

Booking Submission

When a customer confirms a booking through the chat, Booker4j saves it locally and can also submit it to an external system. Different customers can use different submission methods — one might call a REST API, another might only need local storage. This is all controlled through configuration.


How it works

After a customer confirms their booking:

  1. The booking is always saved to MongoDB first (this is the source of truth).
  2. Booker4j checks if the customer has a submission provider configured.
  3. If yes, it converts the booking data into the format the external system expects and submits it.
  4. The result (success, failure, or "no submission configured") is passed to the LLM, which composes a natural confirmation message for the customer.
Customer confirms
|
v
Save to MongoDB (always)
|
v
Submission configured for this customer?
| |
YES NO
| |
v v
Convert DTO Resolve success message
| from translation key
v (default: chat.booking.submission.none.success)
Submit to external system
| |
SUCCESS FAILURE
| |
v v
Resolve Resolve failure message
success from translation key
message (per locale)
from key
(per locale)
| |
v v
LLM composes natural response using the resolved message

The customer always gets a confirmation — even if external submission fails, the booking is safe in MongoDB. The confirmation message shown to the user is not hardcoded — it is resolved from a translation key configured per customer, then passed as context to the LLM which composes a natural response in the conversation language.


Current providers

ProviderDescription
APISubmits to an external REST API via SubmitBookingApi
NOOPNo external submission — booking is only saved locally in MongoDB

If no provider is configured for a customer, the system skips submission entirely (same behavior as NOOP, but without requiring explicit configuration).


Configuring a customer

1. Add the customer to application.yml

Under the chat section, add a booking-submission block:

chat:
booking-submission:
customers:
YOUR_CUSTOMER_ID:
provider: API # or NOOP
converter: stub # converter ID to use
success-message-key: chat.booking.submission.api.success
failure-message-key: chat.booking.submission.failure

Example with multiple customers:

chat:
booking-submission:
customers:
SE00123:
provider: API
converter: cleanhq-booking
success-message-key: chat.booking.submission.api.success
failure-message-key: chat.booking.submission.failure
NO00456:
provider: API
converter: stub
success-message-key: chat.booking.submission.api.success
failure-message-key: chat.booking.submission.failure
DEMO001:
provider: NOOP
converter: stub
success-message-key: chat.booking.submission.noop.success
failure-message-key: chat.booking.submission.failure

2. Configuration fields

FieldRequiredDescription
providerYesThe submission method: API, NOOP
converterYesThe ID of the DTO converter to use (e.g. stub, cleanhq-booking)
success-message-keyNoTranslation key for the success message shown after submission. Defaults to chat.booking.submission.none.success
failure-message-keyNoTranslation key for the failure message. Defaults to chat.booking.submission.failure
configNoOptional key-value map for provider-specific settings

3. What happens if a customer is not listed?

No submission is attempted. The booking is saved to MongoDB and the customer gets a standard confirmation. This is the safe default — you only need to add customers here when you want external submission.

4. Startup validation

When the application starts, it checks that every configured customer has:

  • A registered provider bean for the specified provider type
  • A registered converter bean for the specified converter ID

If either is missing, the application fails to start. This catches misconfiguration early rather than at runtime.


Converters

A converter transforms the internal FormDto (the raw form answers) into whatever DTO the external system expects. The provider and converter are independent — different customers can share the same provider but use different converters.

Current converters

Converter IDDescription
stubReturns an empty BookingDTO. Temporary placeholder — actual field mapping is a separate task.

Creating a new converter

  1. Create a new class that implements BookingDtoConverter:
package com.prasannjeet.booker4j.chat.submission;

import com.prasannjeet.booker4j.form.model.FormDto;
import com.prasannjeet.cleanhq.core.api.dto.BookingDTO;

import org.springframework.stereotype.Component;

@Component
public class MyCustomerBookingDtoConverter implements BookingDtoConverter {

@Override
public String getConverterId() {
return "my-customer-booking"; // This ID is referenced in application.yml
}

@Override
public Object convert(FormDto formDto) {
// Map form fields to the target DTO
var fields = formDto.getFields();
return new BookingDTO()
.typeOfCleaningService((String) fields.get("serviceType"))
.preferredDate(LocalDate.parse((String) fields.get("date")))
.preferredTime(LocalTime.parse((String) fields.get("time")))
.fullName((String) fields.get("fullName"))
.emailAddress((String) fields.get("email"))
.phoneNumber((String) fields.get("phone"))
.address((String) fields.get("address"))
.postalCode((String) fields.get("postalCode"))
.city((String) fields.get("city"));
}
}
  1. Reference it in application.yml:
chat:
booking-submission:
customers:
MY_CUSTOMER:
provider: API
converter: my-customer-booking

That's it. Spring auto-discovers the @Component and the registry picks it up at startup.


Adding a new submission provider

If you need a submission method beyond REST API (e.g. webhook, email, message queue), you need to implement a new provider.

Step 1 — Add the type to the enum

In BookingSubmissionProviderType.java, add your new type:

public enum BookingSubmissionProviderType {
API("Submit booking via external REST API"),
NOOP("No external submission - local persistence only"),
WEBHOOK("Submit booking via webhook callback");
}

Step 2 — Implement the provider

Create a new class that implements BookingSubmissionProvider:

package com.prasannjeet.booker4j.chat.submission;

import static com.prasannjeet.booker4j.chat.submission.BookingSubmissionProviderType.WEBHOOK;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class WebhookBookingSubmissionProvider implements BookingSubmissionProvider {

private final WebClient webClient; // or whatever HTTP client you prefer

@Override
public boolean supports(BookingSubmissionProviderType type) {
return type == WEBHOOK;
}

@Override
public BookingSubmissionResult submit(Object convertedDto, SubmissionContext context) {
// The convertedDto has already been transformed by the converter
// Submit it to the webhook URL
log.info("Submitting booking via webhook for customer: {}", context.customerId());

// Your submission logic here...

return BookingSubmissionResult.builder()
.submitted(true)
.success(true)
.providerType(WEBHOOK.name())
.submissionDetails("Booking submitted successfully via webhook.")
.build();
}
}

Step 3 — Configure a customer to use it

chat:
booking-submission:
customers:
WEBHOOK_CUSTOMER:
provider: WEBHOOK
converter: my-converter

No other wiring is needed. The @Component annotation makes Spring discover the bean, and the registry automatically registers it for the WEBHOOK type at startup.

Step 4 — Add translation keys for the new provider

Add success/failure messages to the chat translation files (src/main/resources/translations/{locale}/chat.yaml):

# In translations/en/chat.yaml, under chat.booking.submission:
booking:
submission:
webhook:
success: "The booking was submitted via webhook and will be processed shortly."
# In translations/sv/chat.yaml, under chat.booking.submission:
booking:
submission:
webhook:
success: "Bokningen skickades via webhook och kommer att behandlas inom kort."

Then reference the key in the customer config:

chat:
booking-submission:
customers:
WEBHOOK_CUSTOMER:
provider: WEBHOOK
converter: my-converter
success-message-key: chat.booking.submission.webhook.success
failure-message-key: chat.booking.submission.failure

Key points when implementing a provider

  • The convertedDto parameter is already converted by the customer's configured converter. The provider receives the final DTO and does not need to know about FormDto.
  • If submission fails, throw an exception. The ConfirmationHandler catches it, logs the error, and returns a graceful failure result to the customer. The booking is safe in MongoDB regardless.
  • The submissionDetails field in the result is factual/technical context for the LLM (e.g. "Structured booking successfully submitted."). The user-facing message comes from the translation key, not from submissionDetails.

Architecture overview

The system follows the Strategy/Registry pattern used elsewhere in Booker4j (same as KnowledgeBaseProviderRegistry).

ConfirmationHandler
|
+--> BookingSubmissionProviderRegistry.resolve(customerId)
|
+--> Reads BookingSubmissionProperties (YAML config)
|
+--> Returns ResolvedSubmission { provider, converter }
|
+--> converter.convert(formDto) --> Object (target DTO)
|
+--> provider.submit(convertedDto, context) --> BookingSubmissionResult

Files

FileRole
chat/submission/BookingSubmissionProviderType.javaEnum of available provider types
chat/submission/BookingSubmissionProvider.javaProvider interface
chat/submission/BookingDtoConverter.javaConverter interface
chat/submission/BookingSubmissionProperties.javaYAML config binding
chat/submission/BookingSubmissionProviderRegistry.javaRegistry — resolves provider + converter per customer
chat/submission/BookingSubmissionResult.javaResult record passed back to handler
chat/submission/SubmissionContext.javaRequest-scoped context (customerId, sessionId, locale, bookingId)
chat/submission/ApiBookingSubmissionProvider.javaAPI provider implementation
chat/submission/NoopBookingSubmissionProvider.javaNo-op provider implementation
chat/submission/StubBookingDtoConverter.javaTemporary stub converter
chat/handler/ConfirmationHandler.javaIntegration point — calls the registry after persisting

Confirmation messages (i18n)

After submission, the LLM composes a natural confirmation message for the user. The content of that message is driven by translation keys configured per customer, not hardcoded strings in the provider.

Built-in translation keys

KeyUsage
chat.booking.submission.api.successAPI submission succeeded
chat.booking.submission.noop.successNOOP provider (local only)
chat.booking.submission.none.successNo provider configured (default fallback)
chat.booking.submission.failureAny submission failure

These are defined in src/main/resources/translations/{locale}/chat.yaml for each supported locale (currently en and sv).

How it works at runtime

  1. After submission, ConfirmationHandler reads the customer's success-message-key or failure-message-key from config.
  2. If the key is not set, it falls back to chat.booking.submission.none.success (success) or chat.booking.submission.failure (failure).
  3. The key is resolved via TranslationService for the current conversation locale.
  4. The resolved message is passed as structured context to the LLM: "Submission status: success. Provider response: ... Message to convey: <resolved translation>".
  5. The LLM composes a natural response incorporating that message.

This means:

  • Different customers can show different confirmation messages even with the same provider.
  • Messages are automatically localized — a Swedish user sees the Swedish version.
  • Adding a new provider only requires adding translation keys and referencing them in config.

Edge cases

ScenarioWhat happens
Customer not in configSubmission skipped. Booking saved to MongoDB. Default success key (chat.booking.submission.none.success) used.
Config has no success-message-keyFalls back to chat.booking.submission.none.success.
Config has no failure-message-keyFalls back to chat.booking.submission.failure.
Translation key missing for localeFalls back to English (default locale). If missing in English too, returns the raw key string.
Config references a provider type with no beanApplication fails to start.
Config references a converter ID with no beanApplication fails to start.
API call fails (network error, 500, etc.)Error logged. Booking safe in MongoDB. Failure message key resolved and conveyed to user.
Stub converter sends empty DTOAPI may reject it (400). Treated as submission failure. Expected until a real converter is implemented.