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:
- The booking is always saved to MongoDB first (this is the source of truth).
- Booker4j checks if the customer has a submission provider configured.
- If yes, it converts the booking data into the format the external system expects and submits it.
- 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
| Provider | Description |
|---|---|
| API | Submits to an external REST API via SubmitBookingApi |
| NOOP | No 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
| Field | Required | Description |
|---|---|---|
provider | Yes | The submission method: API, NOOP |
converter | Yes | The ID of the DTO converter to use (e.g. stub, cleanhq-booking) |
success-message-key | No | Translation key for the success message shown after submission. Defaults to chat.booking.submission.none.success |
failure-message-key | No | Translation key for the failure message. Defaults to chat.booking.submission.failure |
config | No | Optional 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 ID | Description |
|---|---|
stub | Returns an empty BookingDTO. Temporary placeholder — actual field mapping is a separate task. |
Creating a new converter
- 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"));
}
}
- 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
convertedDtoparameter is already converted by the customer's configured converter. The provider receives the final DTO and does not need to know aboutFormDto. - If submission fails, throw an exception. The
ConfirmationHandlercatches it, logs the error, and returns a graceful failure result to the customer. The booking is safe in MongoDB regardless. - The
submissionDetailsfield 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 fromsubmissionDetails.
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
| File | Role |
|---|---|
chat/submission/BookingSubmissionProviderType.java | Enum of available provider types |
chat/submission/BookingSubmissionProvider.java | Provider interface |
chat/submission/BookingDtoConverter.java | Converter interface |
chat/submission/BookingSubmissionProperties.java | YAML config binding |
chat/submission/BookingSubmissionProviderRegistry.java | Registry — resolves provider + converter per customer |
chat/submission/BookingSubmissionResult.java | Result record passed back to handler |
chat/submission/SubmissionContext.java | Request-scoped context (customerId, sessionId, locale, bookingId) |
chat/submission/ApiBookingSubmissionProvider.java | API provider implementation |
chat/submission/NoopBookingSubmissionProvider.java | No-op provider implementation |
chat/submission/StubBookingDtoConverter.java | Temporary stub converter |
chat/handler/ConfirmationHandler.java | Integration 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
| Key | Usage |
|---|---|
chat.booking.submission.api.success | API submission succeeded |
chat.booking.submission.noop.success | NOOP provider (local only) |
chat.booking.submission.none.success | No provider configured (default fallback) |
chat.booking.submission.failure | Any 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
- After submission,
ConfirmationHandlerreads the customer'ssuccess-message-keyorfailure-message-keyfrom config. - If the key is not set, it falls back to
chat.booking.submission.none.success(success) orchat.booking.submission.failure(failure). - The key is resolved via
TranslationServicefor the current conversation locale. - The resolved message is passed as structured context to the LLM:
"Submission status: success. Provider response: ... Message to convey: <resolved translation>". - 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
| Scenario | What happens |
|---|---|
| Customer not in config | Submission skipped. Booking saved to MongoDB. Default success key (chat.booking.submission.none.success) used. |
Config has no success-message-key | Falls back to chat.booking.submission.none.success. |
Config has no failure-message-key | Falls back to chat.booking.submission.failure. |
| Translation key missing for locale | Falls back to English (default locale). If missing in English too, returns the raw key string. |
| Config references a provider type with no bean | Application fails to start. |
| Config references a converter ID with no bean | Application 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 DTO | API may reject it (400). Treated as submission failure. Expected until a real converter is implemented. |