commit 67a10b91563de0877cedcfa382218d46fa0e01ab Author: arjunv Date: Tue Aug 18 09:39:38 2026 +0530 RMC chatbot version 1 implemented diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a154754 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target/ +/.idea/ +*.iml +.DS_Store diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7b016a8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.compile.nullAnalysis.mode": "automatic" +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..04c8f24 --- /dev/null +++ b/README.md @@ -0,0 +1,310 @@ +# WhatsApp Automation + +WhatsApp Automation is a Spring Boot backend for WhatsApp Business messaging workflows. It receives webhook events from Meta, runs bot-driven conversation logic, and persists booking/state data in PostgreSQL. + +## About The Project + +This project provides automation features around WhatsApp messaging: + +- Webhook verification and message handling (`/webhook`) +- Chatbot conversation flow +- PostgreSQL persistence for appointments, service bookings, test bookings, and user state + +The application is a **Java 21** Maven service built with Spring Boot. + +## Tech Stack + +- Spring Boot 3.3.2 +- Java 21 +- Spring Web +- Spring Data JPA +- PostgreSQL +- ngrok (for local webhook tunneling to Meta) + +## Requirements + +| Tool | Purpose | +|------|---------| +| **Java 21** | Runtime for the Spring Boot app | +| **Maven 3.9+** | Build and run the project | +| **PostgreSQL** | Application database | +| **ngrok** | Expose local backend to the public internet so Meta can call your webhook | +| **Meta / WhatsApp Business API access** | Access token, webhook configuration, and test messaging | + +--- + +## How to Run the Backend (Full Setup) + +Follow these steps in order. + +### 1. Clone the repository + +```bash +git clone +cd "WhatsApp Automation" +``` + +Replace `` with the actual Git remote URL for this project. + +--- + +### 2. Install ngrok + +ngrok creates a public HTTPS URL that tunnels to your local Spring Boot server so Meta can deliver WhatsApp webhooks. + +1. Go to [https://ngrok.com](https://ngrok.com) +2. Sign up or sign in +3. Install ngrok for your OS (macOS example with Homebrew): + +```bash +brew install ngrok +``` + +Or download the binary from the ngrok dashboard and follow their install instructions. + +--- + +### 3. Authenticate ngrok with your access token + +After signing in to ngrok, copy your **authtoken** from the ngrok dashboard, then run: + +```bash +ngrok config add-authtoken +``` + +Without this step, ngrok will not start a usable tunnel. + +--- + +### 4. Log in to the WhatsApp Business / Meta developer dashboard + +1. Open the [WhatsApp Business API / Meta for Developers](https://developers.facebook.com/) dashboard +2. Log in with the account that has access to this app +3. Use the project’s WhatsApp / Meta credentials (app ID, business ID, phone number ID, etc.) as configured for your team + +Relevant app links used by this project: + +- **API testing / access token:** + [Generate WhatsApp access token](https://developers.facebook.com/apps/1054675710401577/use_cases/customize/api-testing-v2/?product_route=whatsapp-business&business_id=1051413880774512&use_case_enum=WHATSAPP_BUSINESS_MESSAGING&selected_tab=api-testing-v2) + +- **Webhooks configuration:** + [Configure webhooks](https://developers.facebook.com/apps/1054675710401577/use_cases/customize/webhooks/?product_route=webhooks&business_id=1051413880774512&use_case_enum=WHATSAPP_BUSINESS_MESSAGING&selected_tab=webhooks) + +--- + +### 5. Generate a WhatsApp access token and configure the app + +1. Open the API testing page: + [https://developers.facebook.com/apps/1054675710401577/use_cases/customize/api-testing-v2/?product_route=whatsapp-business&business_id=1051413880774512&use_case_enum=WHATSAPP_BUSINESS_MESSAGING&selected_tab=api-testing-v2](https://developers.facebook.com/apps/1054675710401577/use_cases/customize/api-testing-v2/?product_route=whatsapp-business&business_id=1051413880774512&use_case_enum=WHATSAPP_BUSINESS_MESSAGING&selected_tab=api-testing-v2) +2. Generate a **new Access Token** +3. Open the project config file: + +```text +src/main/resources/application.properties +``` + +4. Paste the token into the access-token property: + +```properties +whatsapp.access-token= +``` + +> **Note:** Temporary tokens from Meta expire. If outbound WhatsApp API calls start failing with auth errors, generate a new token and update this property. + +--- + +### 6. Create the PostgreSQL database and set credentials + +1. Ensure PostgreSQL is installed and running locally +2. Create the database used by this project (default name: `rmc_bot`): + +```bash +createdb rmc_bot +``` + +Or via `psql`: + +```sql +CREATE DATABASE rmc_bot; +``` + +3. Update database settings in `src/main/resources/application.properties`: + +```properties +spring.datasource.url=jdbc:postgresql://localhost:5432/rmc_bot +spring.datasource.username= +spring.datasource.password= +``` + +Default values currently in the file (change these for your machine): + +| Property | Default / example | +|----------|-------------------| +| `spring.datasource.url` | `jdbc:postgresql://localhost:5432/rmc_bot` | +| `spring.datasource.username` | `postgres` | +| `spring.datasource.password` | *(your local password)* | + +The app uses: + +```properties +spring.jpa.hibernate.ddl-auto=update +``` + +so tables are created/updated automatically on startup. + +--- + +### 7. Start the Spring Boot backend + +The server listens on **port 8085** (`server.port=8085`). + +From the project root: + +```bash +mvn spring-boot:run +``` + +Confirm the app started successfully (no datasource or port-binding errors). Leave this process running. + +You can also run tests with: + +```bash +mvn test +``` + +--- + +### 8. Start ngrok and point it at the backend + +**Important:** Start the Spring Boot backend *before* configuring Meta webhooks, and keep both the backend and ngrok running while testing. + +In a **new terminal**: + +```bash +ngrok http 8085 +``` + +ngrok will print a public HTTPS URL, for example: + +```text +https://abcd-1234.ngrok-free.app +``` + +Copy that base URL (the `https://...` forwarding address). + +> Free ngrok URLs change every time you restart ngrok. When the URL changes, you must update the Callback URL in Meta again. + +--- + +### 9. Configure the Meta webhook + +1. Open the webhooks page: + [https://developers.facebook.com/apps/1054675710401577/use_cases/customize/webhooks/?product_route=webhooks&business_id=1051413880774512&use_case_enum=WHATSAPP_BUSINESS_MESSAGING&selected_tab=webhooks](https://developers.facebook.com/apps/1054675710401577/use_cases/customize/webhooks/?product_route=webhooks&business_id=1051413880774512&use_case_enum=WHATSAPP_BUSINESS_MESSAGING&selected_tab=webhooks) + +2. Set **Callback URL** to: + +```text +https:///webhook +``` + +Example: + +```text +https://abcd-1234.ngrok-free.app/webhook +``` + +3. Set **Verify Token** to exactly: + +```text +my_whatsapp_verify_token_123 +``` + +This must match `whatsapp.verify-token` in `application.properties`: + +```properties +whatsapp.verify-token=my_whatsapp_verify_token_123 +``` + +4. Click **Verify and Save** (or equivalent). Meta will send a `GET` request to `/webhook` with `hub.mode`, `hub.verify_token`, and `hub.challenge`. The backend validates the token and returns the challenge. + +5. Subscribe to the webhook fields you need (typically **messages** for inbound WhatsApp traffic). + +--- + +## Configuration reference + +All runtime secrets and connection settings live in: + +```text +src/main/resources/application.properties +``` + +| Property | Description | +|----------|-------------| +| `server.port` | Backend HTTP port (default `8085`) | +| `whatsapp.access-token` | Meta WhatsApp Cloud API access token | +| `whatsapp.verify-token` | Token Meta uses to verify your webhook (`my_whatsapp_verify_token_123`) | +| `spring.datasource.url` | JDBC URL for PostgreSQL | +| `spring.datasource.username` | PostgreSQL username | +| `spring.datasource.password` | PostgreSQL password | +| `spring.jpa.hibernate.ddl-auto` | Schema mode (`update`) | +| `spring.jpa.properties.hibernate.dialect` | PostgreSQL dialect | +| `spring.jpa.show-sql` | Log SQL statements | + +Webhook endpoint exposed by the app: + +| Method | Path | Purpose | +|--------|------|---------| +| `GET` | `/webhook` | Meta webhook verification | +| `POST` | `/webhook` | Inbound WhatsApp events / messages | + +--- + +## Quick checklist + +Use this when bringing the stack up on a new machine: + +1. [ ] Clone the repo +2. [ ] Install and authenticate ngrok +3. [ ] Log in to Meta / WhatsApp Business dashboard +4. [ ] Generate access token → set `whatsapp.access-token` +5. [ ] Create PostgreSQL DB → set datasource username/password +6. [ ] `mvn spring-boot:run` (port **8085**) +7. [ ] `ngrok http 8085` +8. [ ] Meta Callback URL = `https:///webhook` +9. [ ] Verify token = `my_whatsapp_verify_token_123` +10. [ ] Verify webhook + subscribe to message events + +--- + +## Troubleshooting + +| Problem | What to check | +|---------|----------------| +| Webhook verification fails | Verify token must be exactly `my_whatsapp_verify_token_123`. Backend must be running. Callback URL must end with `/webhook`. | +| Meta cannot reach the server | ngrok must be running (`ngrok http 8085`). Use the current ngrok HTTPS URL, not an old one. | +| Database connection errors | PostgreSQL is running; database `rmc_bot` exists; username/password in `application.properties` are correct. | +| Cannot send WhatsApp messages | Access token may have expired — generate a new one and update `whatsapp.access-token`. | +| Port already in use | Something else is bound to `8085`, or change `server.port` and point ngrok at the new port. | +| Not receiving messages on webhook | Your WhatsApp Business Account (WABA) might not be subscribed to your developer App. Link them via API: `curl -X POST "https://graph.facebook.com/v20.0//subscribed_apps" -H "Authorization: Bearer "` | + +--- + +## Project structure (high level) + +```text +src/main/java/com/example/whatsappautomation/ + WhatsAppAutomationApplication.java + controller/WhatsAppWebhookController.java + service/ChatbotService.java + entity/ # Appointment, ServiceBooking, TestBooking, UserState + repository/ # Spring Data JPA repositories +src/main/resources/ + application.properties +``` + +--- + +## License + +Internal / project use unless otherwise specified. diff --git a/app_test.log b/app_test.log new file mode 100644 index 0000000..23ec332 --- /dev/null +++ b/app_test.log @@ -0,0 +1,121 @@ +[INFO] Scanning for projects... +[INFO] +[INFO] ------------------< com.example:whatsapp-automation >------------------- +[INFO] Building WhatsApp Automation 0.0.1-SNAPSHOT +[INFO] from pom.xml +[INFO] --------------------------------[ jar ]--------------------------------- +[INFO] +[INFO] >>> spring-boot:3.3.2:run (default-cli) > test-compile @ whatsapp-automation >>> +[INFO] +[INFO] --- resources:3.3.1:resources (default-resources) @ whatsapp-automation --- +[INFO] Copying 1 resource from src/main/resources to target/classes +[INFO] Copying 3 resources from src/main/resources to target/classes +[INFO] +[INFO] --- compiler:3.13.0:compile (default-compile) @ whatsapp-automation --- +[INFO] Nothing to compile - all classes are up to date. +[INFO] +[INFO] --- resources:3.3.1:testResources (default-testResources) @ whatsapp-automation --- +[INFO] skip non existing resourceDirectory /Users/devprasad/Downloads/RMC-WhatsApp-Bot-main/src/test/resources +[INFO] +[INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ whatsapp-automation --- +[INFO] Nothing to compile - all classes are up to date. +[INFO] +[INFO] <<< spring-boot:3.3.2:run (default-cli) < test-compile @ whatsapp-automation <<< +[INFO] +[INFO] +[INFO] --- spring-boot:3.3.2:run (default-cli) @ whatsapp-automation --- +[INFO] Attaching agents: [] + + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + + :: Spring Boot :: (v3.3.2) + +2026-08-17T11:25:46.813+05:30 INFO 98153 --- [whatsapp-automation] [ main] c.e.w.WhatsAppAutomationApplication : Starting WhatsAppAutomationApplication using Java 26.0.1 with PID 98153 (/Users/devprasad/Downloads/RMC-WhatsApp-Bot-main/target/classes started by devprasad in /Users/devprasad/Downloads/RMC-WhatsApp-Bot-main) +2026-08-17T11:25:46.814+05:30 INFO 98153 --- [whatsapp-automation] [ main] c.e.w.WhatsAppAutomationApplication : No active profile set, falling back to 1 default profile: "default" +2026-08-17T11:25:46.992+05:30 INFO 98153 --- [whatsapp-automation] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode. +2026-08-17T11:25:47.009+05:30 INFO 98153 --- [whatsapp-automation] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 14 ms. Found 4 JPA repository interfaces. +WARNING: A restricted method in java.lang.System has been called +WARNING: java.lang.System::load has been called by org.apache.tomcat.jni.Library in an unnamed module (file:/Users/devprasad/.m2/repository/org/apache/tomcat/embed/tomcat-embed-core/10.1.26/tomcat-embed-core-10.1.26.jar) +WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module +WARNING: Restricted methods will be blocked in a future release unless native access is enabled + +2026-08-17T11:25:47.150+05:30 INFO 98153 --- [whatsapp-automation] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8086 (http) +2026-08-17T11:25:47.153+05:30 INFO 98153 --- [whatsapp-automation] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] +2026-08-17T11:25:47.154+05:30 INFO 98153 --- [whatsapp-automation] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.26] +2026-08-17T11:25:47.167+05:30 INFO 98153 --- [whatsapp-automation] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext +2026-08-17T11:25:47.168+05:30 INFO 98153 --- [whatsapp-automation] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 339 ms +2026-08-17T11:25:47.210+05:30 INFO 98153 --- [whatsapp-automation] [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] +2026-08-17T11:25:47.226+05:30 INFO 98153 --- [whatsapp-automation] [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.5.2.Final +2026-08-17T11:25:47.234+05:30 INFO 98153 --- [whatsapp-automation] [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled +2026-08-17T11:25:47.316+05:30 INFO 98153 --- [whatsapp-automation] [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer +2026-08-17T11:25:47.323+05:30 INFO 98153 --- [whatsapp-automation] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... +2026-08-17T11:25:47.373+05:30 INFO 98153 --- [whatsapp-automation] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@10e9a5fe +2026-08-17T11:25:47.373+05:30 INFO 98153 --- [whatsapp-automation] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. +2026-08-17T11:25:47.387+05:30 WARN 98153 --- [whatsapp-automation] [ main] org.hibernate.orm.deprecation : HHH90000025: PostgreSQLDialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default) +2026-08-17T11:25:47.639+05:30 INFO 98153 --- [whatsapp-automation] [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration) +Hibernate: alter table if exists user_state add column menu_page integer not null +2026-08-17T11:25:47.666+05:30 WARN 98153 --- [whatsapp-automation] [ main] o.h.t.s.i.ExceptionHandlerLoggedImpl : GenerationTarget encountered exception accepting command : Error executing DDL "alter table if exists user_state add column menu_page integer not null" via JDBC [ERROR: column "menu_page" of relation "user_state" contains null values] + +org.hibernate.tool.schema.spi.CommandAcceptanceException: Error executing DDL "alter table if exists user_state add column menu_page integer not null" via JDBC [ERROR: column "menu_page" of relation "user_state" contains null values] + at org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase.accept(GenerationTargetToDatabase.java:94) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] + at org.hibernate.tool.schema.internal.AbstractSchemaMigrator.applySqlString(AbstractSchemaMigrator.java:575) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] + at org.hibernate.tool.schema.internal.AbstractSchemaMigrator.applySqlStrings(AbstractSchemaMigrator.java:515) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] + at org.hibernate.tool.schema.internal.AbstractSchemaMigrator.migrateTable(AbstractSchemaMigrator.java:333) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] + at org.hibernate.tool.schema.internal.GroupedSchemaMigratorImpl.performTablesMigration(GroupedSchemaMigratorImpl.java:84) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] + at org.hibernate.tool.schema.internal.AbstractSchemaMigrator.performMigration(AbstractSchemaMigrator.java:232) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] + at org.hibernate.tool.schema.internal.AbstractSchemaMigrator.doMigration(AbstractSchemaMigrator.java:117) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] + at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.performDatabaseAction(SchemaManagementToolCoordinator.java:280) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] + at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.lambda$process$5(SchemaManagementToolCoordinator.java:144) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] + at java.base/java.util.HashMap.forEach(HashMap.java:1430) ~[na:na] + at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.process(SchemaManagementToolCoordinator.java:141) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] + at org.hibernate.boot.internal.SessionFactoryObserverForSchemaExport.sessionFactoryCreated(SessionFactoryObserverForSchemaExport.java:37) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] + at org.hibernate.internal.SessionFactoryObserverChain.sessionFactoryCreated(SessionFactoryObserverChain.java:35) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] + at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:322) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] + at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:457) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] + at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1506) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] + at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:75) ~[spring-orm-6.1.11.jar:6.1.11] + at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:390) ~[spring-orm-6.1.11.jar:6.1.11] + at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409) ~[spring-orm-6.1.11.jar:6.1.11] + at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396) ~[spring-orm-6.1.11.jar:6.1.11] + at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:366) ~[spring-orm-6.1.11.jar:6.1.11] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1853) ~[spring-beans-6.1.11.jar:6.1.11] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1802) ~[spring-beans-6.1.11.jar:6.1.11] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600) ~[spring-beans-6.1.11.jar:6.1.11] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.11.jar:6.1.11] + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337) ~[spring-beans-6.1.11.jar:6.1.11] + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.11.jar:6.1.11] + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335) ~[spring-beans-6.1.11.jar:6.1.11] + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:205) ~[spring-beans-6.1.11.jar:6.1.11] + at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:954) ~[spring-context-6.1.11.jar:6.1.11] + at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:625) ~[spring-context-6.1.11.jar:6.1.11] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.3.2.jar:3.3.2] + at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[spring-boot-3.3.2.jar:3.3.2] + at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456) ~[spring-boot-3.3.2.jar:3.3.2] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:335) ~[spring-boot-3.3.2.jar:3.3.2] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363) ~[spring-boot-3.3.2.jar:3.3.2] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352) ~[spring-boot-3.3.2.jar:3.3.2] + at com.example.whatsappautomation.WhatsAppAutomationApplication.main(WhatsAppAutomationApplication.java:10) ~[classes/:na] +Caused by: org.postgresql.util.PSQLException: ERROR: column "menu_page" of relation "user_state" contains null values + at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2725) ~[postgresql-42.7.3.jar:42.7.3] + at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2412) ~[postgresql-42.7.3.jar:42.7.3] + at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:371) ~[postgresql-42.7.3.jar:42.7.3] + at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:502) ~[postgresql-42.7.3.jar:42.7.3] + at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:419) ~[postgresql-42.7.3.jar:42.7.3] + at org.postgresql.jdbc.PgStatement.executeWithFlags(PgStatement.java:341) ~[postgresql-42.7.3.jar:42.7.3] + at org.postgresql.jdbc.PgStatement.executeCachedSql(PgStatement.java:326) ~[postgresql-42.7.3.jar:42.7.3] + at org.postgresql.jdbc.PgStatement.executeWithFlags(PgStatement.java:302) ~[postgresql-42.7.3.jar:42.7.3] + at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:297) ~[postgresql-42.7.3.jar:42.7.3] + at com.zaxxer.hikari.pool.ProxyStatement.execute(ProxyStatement.java:94) ~[HikariCP-5.1.0.jar:na] + at com.zaxxer.hikari.pool.HikariProxyStatement.execute(HikariProxyStatement.java) ~[HikariCP-5.1.0.jar:na] + at org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase.accept(GenerationTargetToDatabase.java:80) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] + ... 37 common frames omitted + +2026-08-17T11:25:47.668+05:30 INFO 98153 --- [whatsapp-automation] [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' +2026-08-17T11:25:47.778+05:30 WARN 98153 --- [whatsapp-automation] [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning +2026-08-17T11:25:47.866+05:30 INFO 98153 --- [whatsapp-automation] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8086 (http) with context path '/' +2026-08-17T11:25:47.869+05:30 INFO 98153 --- [whatsapp-automation] [ main] c.e.w.WhatsAppAutomationApplication : Started WhatsAppAutomationApplication in 1.159 seconds (process running for 1.272) diff --git a/fix_args.py b/fix_args.py new file mode 100644 index 0000000..a675111 --- /dev/null +++ b/fix_args.py @@ -0,0 +1,63 @@ +import re + +with open('src/main/java/com/example/whatsappautomation/service/ChatbotService.java', 'r') as f: + content = f.read() + +# We want to match: sendPaginatedButtons(state, arg2, arg3, new String[]{...}, new String[]{...}) +# and replace it with: sendPaginatedButtons(state, arg2, new String[]{...}, new String[]{...}) +# Note: arg2 and arg3 might be getMessage(...) or "string". +# It's safest to match: sendPaginatedButtons(state, (whatever), (whatever), new String[]{ + +def replacer(match): + # match.group(1) is the part from `state, ` up to `, new String[]{` + # this part contains `arg2, arg3`. We need to split by comma from the right, but arg2 or arg3 might have commas! + # Wait, getting the last argument before `new String[]{` is the best way. + # Actually, the button text (arg3) is almost always `getMessage("something.btn", state)` or `"View Options"` or similar. + # Let's just use regex to match the third argument which is always followed by `, new String\[\]\{` + pass + +# Let's write a simple python parser for the arguments of sendPaginatedButtons +new_content = "" +idx = 0 +while True: + start_idx = content.find('sendPaginatedButtons(state, ', idx) + if start_idx == -1: + new_content += content[idx:] + break + + new_content += content[idx:start_idx] + + # find the start of `new String[]{` + arrays_start = content.find('new String[]{', start_idx) + + # The text between `start_idx + len('sendPaginatedButtons(state, ')` and `arrays_start` is `arg2, arg3, ` + # We want to remove `arg3, `. + # `arg3` is either a string literal `"..."` or a `getMessage(...)` call. + args_str = content[start_idx + len('sendPaginatedButtons(state, '):arrays_start].strip() + # args_str ends with `,`. Let's remove the trailing comma. + args_str = args_str[:-1].strip() + + # Now we need to split args_str into arg2 and arg3. + # If it ends with `state)`, arg3 is `getMessage(..., state)`. + # If it ends with `"`, arg3 is a string literal. + + # Actually, we can just find the LAST comma that is NOT inside parentheses! + # A simple comma split works if we respect parens. + def rsplit_args(s): + depth = 0 + for i in range(len(s)-1, -1, -1): + if s[i] == ')': depth += 1 + elif s[i] == '(': depth -= 1 + elif s[i] == ',' and depth == 0: + return s[:i].strip(), s[i+1:].strip() + return s, "" + + arg2, arg3 = rsplit_args(args_str) + + new_content += f"sendPaginatedButtons(state, {arg2}, " + idx = arrays_start + +with open('src/main/java/com/example/whatsappautomation/service/ChatbotService.java', 'w') as f: + f.write(new_content) + +print("Fixed arguments") diff --git a/fix_menu.py b/fix_menu.py new file mode 100644 index 0000000..6e1b5c3 --- /dev/null +++ b/fix_menu.py @@ -0,0 +1,101 @@ +with open("src/main/java/com/example/whatsappautomation/service/ChatbotService.java", "r") as f: + content = f.read() + +old_paginated = """ private void sendPaginatedButtons(UserState state, String bodyText, String[] ids, String[] titles) { + int totalOptions = ids.length; + + // Loop and send chunks of up to 3 buttons + for (int i = 0; i < totalOptions; i += 3) { + int end = Math.min(i + 3, totalOptions); + int count = end - i; + String[] chunkIds = new String[count]; + String[] chunkTitles = new String[count]; + + for (int j = 0; j < count; j++) { + chunkIds[j] = ids[i + j]; + chunkTitles[j] = titles[i + j]; + } + + String chunkBodyText = (i == 0) ? bodyText : ( "en".equals(state.getLanguagePreference()) ? "More options:" : "மேலும் விருப்பங்கள்:" ); + + sendInteractiveButtons(chunkBodyText, chunkIds, chunkTitles); + } + }""" + +new_methods = """ private void sendInteractiveList(String bodyText, String buttonText, String sectionTitle, String[] ids, String[] titles) { + String phoneNumberId = currentPhoneId.get(); + String recipientPhoneNumber = currentFrom.get(); + if (phoneNumberId == null || recipientPhoneNumber == null) return; + String url = "https://graph.facebook.com/v20.0/" + phoneNumberId + "/messages"; + + try { + ObjectNode requestBody = objectMapper.createObjectNode(); + requestBody.put("messaging_product", "whatsapp"); + requestBody.put("recipient_type", "individual"); + requestBody.put("to", recipientPhoneNumber); + requestBody.put("type", "interactive"); + + ObjectNode interactiveNode = objectMapper.createObjectNode(); + interactiveNode.put("type", "list"); + + ObjectNode bodyNode = objectMapper.createObjectNode(); + + UserState uState = userStateRepository.findById(recipientPhoneNumber).orElse(null); + if (uState != null && !"MAIN_MENU".equals(uState.getState()) && !"LANGUAGE_SELECTION".equals(uState.getState())) { + String restartText = "en".equals(uState.getLanguagePreference()) + ? "\\n\\n(Press 0 to restart the conversation)" + : "\\n\\n(உரையாடலை மீண்டும் தொடங்க 0 ஐ அழுத்தவும்)"; + bodyText += restartText; + } + bodyNode.put("text", bodyText); + interactiveNode.set("body", bodyNode); + + ObjectNode actionNode = objectMapper.createObjectNode(); + actionNode.put("button", buttonText); + + com.fasterxml.jackson.databind.node.ArrayNode sectionsArray = actionNode.putArray("sections"); + ObjectNode section = objectMapper.createObjectNode(); + section.put("title", sectionTitle); + com.fasterxml.jackson.databind.node.ArrayNode rowsArray = section.putArray("rows"); + + int limit = Math.min(ids.length, 10); + for (int i = 0; i < limit; i++) { + ObjectNode row = objectMapper.createObjectNode(); + row.put("id", ids[i]); + row.put("title", titles[i]); + rowsArray.add(row); + } + sectionsArray.add(section); + + interactiveNode.set("action", actionNode); + requestBody.set("interactive", interactiveNode); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON); + headers.setBearerAuth(accessToken); + + org.springframework.http.HttpEntity entity = new org.springframework.http.HttpEntity<>(requestBody.toString(), headers); + restTemplate.postForEntity(url, entity, String.class); + } catch (org.springframework.web.client.HttpStatusCodeException e) { + logger.error("Error sending interactive list. Status: {}, Response: {}", e.getStatusCode(), e.getResponseBodyAsString(), e); + } catch (Exception e) { + logger.error("Error sending interactive list", e); + } + } + + private void sendPaginatedButtons(UserState state, String bodyText, String[] ids, String[] titles) { + if (ids.length <= 3) { + sendInteractiveButtons(bodyText, ids, titles); + } else { + String buttonText = "en".equals(state.getLanguagePreference()) ? "Select Option" : "விருப்பத்தை தேர்ந்தெடுக்கவும்"; + String sectionTitle = "en".equals(state.getLanguagePreference()) ? "Options" : "விருப்பங்கள்"; + sendInteractiveList(bodyText, buttonText, sectionTitle, ids, titles); + } + }""" + +content = content.replace(old_paginated, new_methods) + +with open("src/main/java/com/example/whatsappautomation/service/ChatbotService.java", "w") as f: + f.write(content) + +print("Replacement complete.") diff --git a/fix_restart.py b/fix_restart.py new file mode 100644 index 0000000..360767f --- /dev/null +++ b/fix_restart.py @@ -0,0 +1,91 @@ +with open("src/main/java/com/example/whatsappautomation/service/ChatbotService.java", "r") as f: + content = f.read() + +# 1. Modify sendInteractiveButtons to fetch state and append restart text +old_method_start = """ private void sendInteractiveButtons(String bodyText, String[] ids, String[] titles) { + String phoneNumberId = currentPhoneId.get(); + String recipientPhoneNumber = currentFrom.get();""" + +new_method_start = """ private void sendInteractiveButtons(String bodyText, String[] ids, String[] titles) { + String phoneNumberId = currentPhoneId.get(); + String recipientPhoneNumber = currentFrom.get(); + + UserState uState = userStateRepository.findById(recipientPhoneNumber).orElse(null); + if (uState != null && !"MAIN_MENU".equals(uState.getState()) && !"LANGUAGE_SELECTION".equals(uState.getState())) { + String restartText = "en".equals(uState.getLanguagePreference()) + ? "\\n\\n(Press 0 to restart the conversation)" + : "\\n\\n(உரையாடலை மீண்டும் தொடங்க 0 ஐ அழுத்தவும்)"; + bodyText += restartText; + }""" + +content = content.replace(old_method_start, new_method_start) + +# 2. Modify sendPaginatedButtons to not append restart text itself +old_paginated = """ private void sendPaginatedButtons(UserState state, String bodyText, String[] ids, String[] titles) { + int totalOptions = ids.length; + String restartText = "en".equals(state.getLanguagePreference()) + ? "\\n\\n(Send 0 to go back to the main menu / Restart the conversation)" + : "\\n\\n(உரையாடலை மீண்டும் தொடங்க 0 ஐ அழுத்தவும்)"; + + // Loop and send chunks of up to 3 buttons + for (int i = 0; i < totalOptions; i += 3) { + int end = Math.min(i + 3, totalOptions); + int count = end - i; + String[] chunkIds = new String[count]; + String[] chunkTitles = new String[count]; + + for (int j = 0; j < count; j++) { + chunkIds[j] = ids[i + j]; + chunkTitles[j] = titles[i + j]; + } + + String chunkBodyText = (i == 0) ? bodyText : ( "en".equals(state.getLanguagePreference()) ? "More options:" : "மேலும் விருப்பங்கள்:" ); + if (end == totalOptions) { + chunkBodyText += restartText; + } + + sendInteractiveButtons(chunkBodyText, chunkIds, chunkTitles); + } + }""" + +new_paginated = """ private void sendPaginatedButtons(UserState state, String bodyText, String[] ids, String[] titles) { + int totalOptions = ids.length; + + // Loop and send chunks of up to 3 buttons + for (int i = 0; i < totalOptions; i += 3) { + int end = Math.min(i + 3, totalOptions); + int count = end - i; + String[] chunkIds = new String[count]; + String[] chunkTitles = new String[count]; + + for (int j = 0; j < count; j++) { + chunkIds[j] = ids[i + j]; + chunkTitles[j] = titles[i + j]; + } + + String chunkBodyText = (i == 0) ? bodyText : ( "en".equals(state.getLanguagePreference()) ? "More options:" : "மேலும் விருப்பங்கள்:" ); + + sendInteractiveButtons(chunkBodyText, chunkIds, chunkTitles); + } + }""" + +content = content.replace(old_paginated, new_paginated) + +# 3. Change processMessage to say "Press 0 to restart the conversation" instead of "(Send 0 to go back to the main menu / Restart the conversation)" for text responses +old_process_text = """ if (response != null && !response.isEmpty() && !response.startsWith("INTERACTIVE:") && !userState.getState().equals("MAIN_MENU") && !userState.getState().equals("LANGUAGE_SELECTION")) { + response += "\\n\\n(Send 0 to go back to the main menu / Restart the conversation)"; + }""" + +new_process_text = """ if (response != null && !response.isEmpty() && !response.startsWith("INTERACTIVE:") && !userState.getState().equals("MAIN_MENU") && !userState.getState().equals("LANGUAGE_SELECTION")) { + String rt = "en".equals(userState.getLanguagePreference()) + ? "\\n\\n(Press 0 to restart the conversation)" + : "\\n\\n(உரையாடலை மீண்டும் தொடங்க 0 ஐ அழுத்தவும்)"; + response += rt; + }""" + +content = content.replace(old_process_text, new_process_text) + +with open("src/main/java/com/example/whatsappautomation/service/ChatbotService.java", "w") as f: + f.write(content) + +print("Replacement complete.") diff --git a/fix_tamil.py b/fix_tamil.py new file mode 100644 index 0000000..c85164c --- /dev/null +++ b/fix_tamil.py @@ -0,0 +1,38 @@ +replacements = { + "காய்ச்சல் பரிசோதனைகள்": "காய்ச்சல் பரிசோதனை", + "மூட்டு மற்றும் எலும்பு": "மூட்டு & எலும்பு", + "முழுமையான உடல் பரிசோதனை": "முழு உடல் பரிசோதனை", + "காய்ச்சல் அல்ட்ரா பேனல்": "காய்ச்சல் அல்ட்ரா", + "மலம் பரிசோதனை (Stool Occult Blood)": "மலம் பரிசோதனை", + "அமயா இன்ட் கார்டியாக்": "அமயா கார்டியாக்", + "கார்டியாக் பேக்கேஜ் - அமயா": "அமயா பேக்கேஜ்", + "உயர் இரத்த அழுத்தம் தொகுப்பு": "உயர் ரத்த அழுத்தம்", + "மாஸ்டர் ஹெல்த் பேசிக்": "மாஸ்டர் பேசிக்", + "மாஸ்டர் ஹெல்த் எக்ஸிகியூட்டிவ்": "மாஸ்டர் எக்ஸிகியூட்", + "மாஸ்டர் ஹெல்த் பிரீமியம்": "மாஸ்டர் பிரீமியம்", + "கர்ப்ப காலம் அட்வான்ஸ்டு": "கர்ப்ப அட்வான்ஸ்டு", + "சர்க்கரை கட்டுப்பாடு பேசிக்": "சர்க்கரை பேசிக்", + "சர்க்கரை கட்டுப்பாடு பிளஸ்": "சர்க்கரை பிளஸ்", + "பெண்கள் நல்வாழ்வு பேசிக்": "பெண்கள் பேசிக்", + "பெண்கள் நல்வாழ்வு அட்வான்ஸ்டு": "பெண்கள் அட்வான்ஸ்டு", + "புதிய சந்திப்பை முன்பதிவு செய்ய": "புதிய முன்பதிவு", + "சந்திப்புகளைப் பார்க்க": "சந்திப்புகளை பார்", + "சந்திப்பை மாற்றியமைக்க": "மாற்றியமைக்க", + "சந்திப்பை ரத்து செய்ய": "ரத்து செய்ய", + "முன்பதிவை உறுதிப்படுத்தவும்": "உறுதிப்படுத்துக", + "சந்திப்பை ரத்து செய்யவும்": "ரத்து செய்க", + "கட்டு போடுதல் (Dressing)": "கட்டு போடுதல்", + "வடிகுழாய் (Catheterization)": "வடிகுழாய்", + "வீட்டு பராமரிப்பு தொகுப்புகள்": "வீட்டு பராமரிப்பு" +} + +with open('src/main/resources/messages_ta.properties', 'r', encoding='utf-8') as f: + content = f.read() + +for old, new in replacements.items(): + content = content.replace(f"={old}", f"={new}") + +with open('src/main/resources/messages_ta.properties', 'w', encoding='utf-8') as f: + f.write(content) + +print("Replaced lengths.") diff --git a/length_check.py b/length_check.py new file mode 100644 index 0000000..57dbfda --- /dev/null +++ b/length_check.py @@ -0,0 +1,15 @@ +import re + +# Keys that are used for buttons (options) +button_keys = ['opt', 'btn', 'yes', 'no', 'confirm', 'change', 'cancel'] + +with open('src/main/resources/messages_ta.properties', 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if not line or line.startswith('#'): + continue + if '=' in line: + key, val = line.split('=', 1) + is_button = any(bk in key for bk in button_keys) + if is_button and len(val) > 20: + print(f"{key}={val} ({len(val)} chars)") diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..64ef23d --- /dev/null +++ b/pom.xml @@ -0,0 +1,56 @@ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + + com.example + whatsapp-automation + 0.0.1-SNAPSHOT + WhatsApp Automation + Spring Boot application for WhatsApp automation + + + 21 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.postgresql + postgresql + runtime + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/replace.py b/replace.py new file mode 100644 index 0000000..6441269 --- /dev/null +++ b/replace.py @@ -0,0 +1,54 @@ +import re + +with open('src/main/java/com/example/whatsappautomation/service/ChatbotService.java', 'r') as f: + content = f.read() + +# Pattern to match sendInteractiveList(...) up to the null); +# We need to capture bodyText (arg 1), ids (arg 4), titles (arg 5). +# Using a python script to parse the balanced parentheses might be better. + +def replace_calls(text): + start = 0 + while True: + idx = text.find('sendInteractiveList(', start) + if idx == -1: + break + + # find the end of this statement + end_idx = text.find(');', idx) + if end_idx == -1: + break + + call_str = text[idx:end_idx+2] + + # We need to extract the parts. It's safer to just split by "new String[]{" + # because we know args 4 and 5 are array allocations. + + parts = call_str.split('new String[]{') + if len(parts) == 3: + # parts[0] has 'sendInteractiveList(bodyText, buttonText, sectionTitle, \n' + # parts[1] has 'ids...}, \n' + # parts[2] has 'titles...}, null);' + + # Extract bodyText from parts[0] + # sendInteractiveList(arg1, arg2, arg3, + # We want just arg1! + p0 = parts[0][len('sendInteractiveList('):].strip() + # It's tricky to parse commas. + pass + +print("Doing regex...") + +# Actually, let's just use a simple regex if they are formatted consistently. +# Almost all of them are: +# sendInteractiveList( BODY, BTN, TITLE, +# new String[]{IDS}, +# new String[]{TITLES}, null); + +pattern = r'sendInteractiveList\((.*?),\s*[^,]+,\s*[^,]+,\s*(new String\[\]\{.*?\})\s*,\s*(new String\[\]\{.*?\})\s*,\s*null\);' +replaced = re.sub(pattern, r'sendPaginatedButtons(state, \1, \2, \3);', content, flags=re.DOTALL) + +with open('src/main/java/com/example/whatsappautomation/service/ChatbotService.java', 'w') as f: + f.write(replaced) + +print("Done") diff --git a/src/main/java/com/example/whatsappautomation/WhatsAppAutomationApplication.java b/src/main/java/com/example/whatsappautomation/WhatsAppAutomationApplication.java new file mode 100644 index 0000000..3f9a9d5 --- /dev/null +++ b/src/main/java/com/example/whatsappautomation/WhatsAppAutomationApplication.java @@ -0,0 +1,12 @@ +package com.example.whatsappautomation; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class WhatsAppAutomationApplication { + + public static void main(String[] args) { + SpringApplication.run(WhatsAppAutomationApplication.class, args); + } +} diff --git a/src/main/java/com/example/whatsappautomation/controller/WhatsAppWebhookController.java b/src/main/java/com/example/whatsappautomation/controller/WhatsAppWebhookController.java new file mode 100644 index 0000000..717298c --- /dev/null +++ b/src/main/java/com/example/whatsappautomation/controller/WhatsAppWebhookController.java @@ -0,0 +1,99 @@ +package com.example.whatsappautomation.controller; + +import com.example.whatsappautomation.service.ChatbotService; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/webhook") +public class WhatsAppWebhookController { + + private static final Logger logger = LoggerFactory.getLogger(WhatsAppWebhookController.class); + + @Value("${whatsapp.verify-token}") + private String verifyToken; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + private final ChatbotService chatbotService; + + public WhatsAppWebhookController(ChatbotService chatbotService) { + this.chatbotService = chatbotService; + } + + /** + * Webhook verification endpoint for Meta/WhatsApp. + */ + @GetMapping + public ResponseEntity verifyWebhook( + @RequestParam("hub.mode") String mode, + @RequestParam("hub.challenge") String challenge, + @RequestParam("hub.verify_token") String token) { + + logger.info("Received verification request: mode={}, token={}", mode, token); + + if ("subscribe".equals(mode) && verifyToken.equals(token)) { + logger.info("Webhook verified successfully!"); + return ResponseEntity.ok(challenge); + } else { + logger.warn("Webhook verification failed. Provided token: {}", token); + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Verification failed"); + } + } + + /** + * Webhook event handler endpoint. + */ + @PostMapping + public ResponseEntity receive(@RequestBody String body) { + System.out.println("Received:"); + System.out.println(body); + + try { + JsonNode payload = objectMapper.readTree(body); + if (payload.has("entry")) { + JsonNode entryArray = payload.get("entry"); + if (entryArray.isArray() && entryArray.size() > 0) { + JsonNode changesArray = entryArray.get(0).get("changes"); + if (changesArray != null && changesArray.isArray() && changesArray.size() > 0) { + JsonNode value = changesArray.get(0).get("value"); + if (value != null && value.has("messages")) { + JsonNode messagesArray = value.get("messages"); + if (messagesArray.isArray() && messagesArray.size() > 0) { + JsonNode message = messagesArray.get(0); + String from = message.get("from").asText(); + + // Avoid processing status changes/read receipts as incoming messages + String textBody = null; + if (message.has("text")) { + textBody = message.get("text").get("body").asText(); + } else if (message.has("interactive")) { + JsonNode interactive = message.get("interactive"); + if (interactive.has("button_reply")) { + textBody = interactive.get("button_reply").get("id").asText(); + } else if (interactive.has("list_reply")) { + textBody = interactive.get("list_reply").get("id").asText(); + } + } + if (textBody != null) { + String phoneNumberId = value.get("metadata").get("phone_number_id").asText(); + // Delegate to Chatbot Service + chatbotService.processMessage(from, textBody, phoneNumberId); + } + } + } + } + } + } + } catch (Exception e) { + logger.error("Error processing webhook payload", e); + } + + return ResponseEntity.ok("EVENT_RECEIVED"); + } +} diff --git a/src/main/java/com/example/whatsappautomation/entity/Appointment.java b/src/main/java/com/example/whatsappautomation/entity/Appointment.java new file mode 100644 index 0000000..02d782e --- /dev/null +++ b/src/main/java/com/example/whatsappautomation/entity/Appointment.java @@ -0,0 +1,44 @@ +package com.example.whatsappautomation.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "appointment") +public class Appointment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String phoneNumber; + private String patientId; + private String appointmentId; + private String department; + private String doctor; + private String appointmentDate; + private String appointmentTime; + private String fee; + private String status; + + // Getters and Setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getPhoneNumber() { return phoneNumber; } + public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } + public String getPatientId() { return patientId; } + public void setPatientId(String patientId) { this.patientId = patientId; } + public String getAppointmentId() { return appointmentId; } + public void setAppointmentId(String appointmentId) { this.appointmentId = appointmentId; } + public String getDepartment() { return department; } + public void setDepartment(String department) { this.department = department; } + public String getDoctor() { return doctor; } + public void setDoctor(String doctor) { this.doctor = doctor; } + public String getAppointmentDate() { return appointmentDate; } + public void setAppointmentDate(String appointmentDate) { this.appointmentDate = appointmentDate; } + public String getAppointmentTime() { return appointmentTime; } + public void setAppointmentTime(String appointmentTime) { this.appointmentTime = appointmentTime; } + public String getFee() { return fee; } + public void setFee(String fee) { this.fee = fee; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } +} diff --git a/src/main/java/com/example/whatsappautomation/entity/ServiceBooking.java b/src/main/java/com/example/whatsappautomation/entity/ServiceBooking.java new file mode 100644 index 0000000..44b5d5f --- /dev/null +++ b/src/main/java/com/example/whatsappautomation/entity/ServiceBooking.java @@ -0,0 +1,44 @@ +package com.example.whatsappautomation.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "service_booking") +public class ServiceBooking { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String phoneNumber; + private String patientId; + private String bookingId; + private String serviceName; + private String price; + private String address; + private String bookingDate; + private String bookingTime; + private String status; + + // Getters and Setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getPhoneNumber() { return phoneNumber; } + public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } + public String getPatientId() { return patientId; } + public void setPatientId(String patientId) { this.patientId = patientId; } + public String getBookingId() { return bookingId; } + public void setBookingId(String bookingId) { this.bookingId = bookingId; } + public String getServiceName() { return serviceName; } + public void setServiceName(String serviceName) { this.serviceName = serviceName; } + public String getPrice() { return price; } + public void setPrice(String price) { this.price = price; } + public String getAddress() { return address; } + public void setAddress(String address) { this.address = address; } + public String getBookingDate() { return bookingDate; } + public void setBookingDate(String bookingDate) { this.bookingDate = bookingDate; } + public String getBookingTime() { return bookingTime; } + public void setBookingTime(String bookingTime) { this.bookingTime = bookingTime; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } +} diff --git a/src/main/java/com/example/whatsappautomation/entity/TestBooking.java b/src/main/java/com/example/whatsappautomation/entity/TestBooking.java new file mode 100644 index 0000000..4a470ec --- /dev/null +++ b/src/main/java/com/example/whatsappautomation/entity/TestBooking.java @@ -0,0 +1,47 @@ +package com.example.whatsappautomation.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "test_booking") +public class TestBooking { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String phoneNumber; + private String bookingId; + private String packageName; + private String price; + private String collectionType; + private String address; + private String bookingDate; + private String bookingTime; + private String patientId; + private String status; + + // Getters and Setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + public String getPhoneNumber() { return phoneNumber; } + public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } + public String getBookingId() { return bookingId; } + public void setBookingId(String bookingId) { this.bookingId = bookingId; } + public String getPackageName() { return packageName; } + public void setPackageName(String packageName) { this.packageName = packageName; } + public String getPrice() { return price; } + public void setPrice(String price) { this.price = price; } + public String getCollectionType() { return collectionType; } + public void setCollectionType(String collectionType) { this.collectionType = collectionType; } + public String getAddress() { return address; } + public void setAddress(String address) { this.address = address; } + public String getBookingDate() { return bookingDate; } + public void setBookingDate(String bookingDate) { this.bookingDate = bookingDate; } + public String getBookingTime() { return bookingTime; } + public void setBookingTime(String bookingTime) { this.bookingTime = bookingTime; } + public String getPatientId() { return patientId; } + public void setPatientId(String patientId) { this.patientId = patientId; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } +} diff --git a/src/main/java/com/example/whatsappautomation/entity/UserState.java b/src/main/java/com/example/whatsappautomation/entity/UserState.java new file mode 100644 index 0000000..baf92a2 --- /dev/null +++ b/src/main/java/com/example/whatsappautomation/entity/UserState.java @@ -0,0 +1,162 @@ +package com.example.whatsappautomation.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "user_state") +public class UserState { + + @Id + private String phoneNumber; + + private String patientId; + private String state; + private String category; + private String subCategory; + private String selectedItem; + private String price; + private String address; + private String bookingDate; + private String bookingTime; + private String languagePreference = "en"; + private Integer menuPage = 0; + private String lastPhoneNumberId; + private Long lastInteractionTime; + + public UserState() {} + + public UserState(String phoneNumber) { + this.phoneNumber = phoneNumber; + this.state = "LANGUAGE_SELECTION"; + this.patientId = "PAT-" + (100000 + new java.util.Random().nextInt(900000)); + } + + public void reset() { + this.state = "LANGUAGE_SELECTION"; + this.category = null; + this.subCategory = null; + this.selectedItem = null; + this.price = null; + this.address = null; + this.bookingDate = null; + this.bookingTime = null; + this.menuPage = 0; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public void setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + public String getState() { + return state; + } + + public void setState(String state) { + if (!state.equals(this.state)) { + this.menuPage = 0; + } + this.state = state; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getSubCategory() { + return subCategory; + } + + public void setSubCategory(String subCategory) { + this.subCategory = subCategory; + } + + public String getSelectedItem() { + return selectedItem; + } + + public void setSelectedItem(String selectedItem) { + this.selectedItem = selectedItem; + } + + public String getPrice() { + return price; + } + + public void setPrice(String price) { + this.price = price; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public String getBookingDate() { + return bookingDate; + } + + public void setBookingDate(String bookingDate) { + this.bookingDate = bookingDate; + } + + public String getBookingTime() { + return bookingTime; + } + + public void setBookingTime(String bookingTime) { + this.bookingTime = bookingTime; + } + + public String getPatientId() { + return patientId; + } + + public void setPatientId(String patientId) { + this.patientId = patientId; + } + + public String getLanguagePreference() { + return languagePreference; + } + + public void setLanguagePreference(String languagePreference) { + this.languagePreference = languagePreference; + } + + public Integer getMenuPage() { + return menuPage; + } + + public void setMenuPage(Integer menuPage) { + this.menuPage = menuPage; + } + + public String getLastPhoneNumberId() { + return lastPhoneNumberId; + } + + public void setLastPhoneNumberId(String lastPhoneNumberId) { + this.lastPhoneNumberId = lastPhoneNumberId; + } + + public Long getLastInteractionTime() { + return lastInteractionTime; + } + + public void setLastInteractionTime(Long lastInteractionTime) { + this.lastInteractionTime = lastInteractionTime; + } +} diff --git a/src/main/java/com/example/whatsappautomation/repository/AppointmentRepository.java b/src/main/java/com/example/whatsappautomation/repository/AppointmentRepository.java new file mode 100644 index 0000000..f8df56f --- /dev/null +++ b/src/main/java/com/example/whatsappautomation/repository/AppointmentRepository.java @@ -0,0 +1,14 @@ +package com.example.whatsappautomation.repository; + +import com.example.whatsappautomation.entity.Appointment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface AppointmentRepository extends JpaRepository { + List findByPhoneNumber(String phoneNumber); + Optional findByAppointmentId(String appointmentId); +} diff --git a/src/main/java/com/example/whatsappautomation/repository/ServiceBookingRepository.java b/src/main/java/com/example/whatsappautomation/repository/ServiceBookingRepository.java new file mode 100644 index 0000000..d31cdae --- /dev/null +++ b/src/main/java/com/example/whatsappautomation/repository/ServiceBookingRepository.java @@ -0,0 +1,9 @@ +package com.example.whatsappautomation.repository; + +import com.example.whatsappautomation.entity.ServiceBooking; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ServiceBookingRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/whatsappautomation/repository/TestBookingRepository.java b/src/main/java/com/example/whatsappautomation/repository/TestBookingRepository.java new file mode 100644 index 0000000..7faeb96 --- /dev/null +++ b/src/main/java/com/example/whatsappautomation/repository/TestBookingRepository.java @@ -0,0 +1,9 @@ +package com.example.whatsappautomation.repository; + +import com.example.whatsappautomation.entity.TestBooking; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TestBookingRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/whatsappautomation/repository/UserStateRepository.java b/src/main/java/com/example/whatsappautomation/repository/UserStateRepository.java new file mode 100644 index 0000000..77ce3d4 --- /dev/null +++ b/src/main/java/com/example/whatsappautomation/repository/UserStateRepository.java @@ -0,0 +1,9 @@ +package com.example.whatsappautomation.repository; + +import com.example.whatsappautomation.entity.UserState; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserStateRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/whatsappautomation/service/ChatbotService.java b/src/main/java/com/example/whatsappautomation/service/ChatbotService.java new file mode 100644 index 0000000..3a24d94 --- /dev/null +++ b/src/main/java/com/example/whatsappautomation/service/ChatbotService.java @@ -0,0 +1,2091 @@ +package com.example.whatsappautomation.service; + +import com.example.whatsappautomation.entity.*; +import com.example.whatsappautomation.repository.*; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.context.MessageSource; + +import java.util.List; +import java.util.Optional; +import java.util.Random; + +@Service +public class ChatbotService { + private static final Logger logger = LoggerFactory.getLogger(ChatbotService.class); + + private final ThreadLocal currentPhoneId = new ThreadLocal<>(); + private final ThreadLocal currentFrom = new ThreadLocal<>(); + + private final UserStateRepository userStateRepository; + private final ServiceBookingRepository serviceBookingRepository; + private final TestBookingRepository testBookingRepository; + private final AppointmentRepository appointmentRepository; + private final MessageSource messageSource; + + @Value("${whatsapp.access-token}") + private String accessToken; + + @Value("${conversation.inactivity.timeout:120}") + private int timeoutSeconds = 120; + + @Value("${chatbot.new-workflow.enabled:false}") + private boolean newWorkflowEnabled; + + private final java.util.concurrent.ScheduledExecutorService scheduler = java.util.concurrent.Executors.newScheduledThreadPool(2); + private final java.util.concurrent.ConcurrentHashMap> scheduledTasks = new java.util.concurrent.ConcurrentHashMap<>(); + + private final RestTemplate restTemplate = new RestTemplate(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + public ChatbotService(UserStateRepository userStateRepository, + ServiceBookingRepository serviceBookingRepository, + TestBookingRepository testBookingRepository, + AppointmentRepository appointmentRepository, + MessageSource messageSource) { + this.userStateRepository = userStateRepository; + this.serviceBookingRepository = serviceBookingRepository; + this.testBookingRepository = testBookingRepository; + this.appointmentRepository = appointmentRepository; + this.messageSource = messageSource; + } + + public void processMessage(String from, String messageText, String phoneNumberId) { + currentPhoneId.set(phoneNumberId); + currentFrom.set(from); + try { + UserState userState = userStateRepository.findById(from).orElse(new UserState(from)); + + userState.setLastPhoneNumberId(phoneNumberId); + userState.setLastInteractionTime(System.currentTimeMillis()); + + cancelTimeoutTask(from); + + String lowerInput = messageText.trim().toLowerCase(); + boolean isGreeting = lowerInput.equals("hi") || lowerInput.equals("hello") || lowerInput.equals("hey") || lowerInput.equals("0"); + + if (isGreeting) { + userState.reset(); + } else if ("EXPIRED".equals(userState.getState())) { + String lang = userState.getLanguagePreference(); + String reply; + if ("ta".equalsIgnoreCase(lang)) { + reply = "👋 புதிய உரையாடலைத் தொடங்க Hi அல்லது Hello என டைப் செய்யவும்."; + } else if ("hi".equalsIgnoreCase(lang)) { + reply = "👋 नई बातचीत शुरू करने के लिए कृपया Hi या Hello टाइप करें।"; + } else { + reply = "👋 To start a new conversation, please type Hi or Hello."; + } + + userStateRepository.save(userState); + sendMessage(phoneNumberId, from, reply); + return; + } + + String response; + if (newWorkflowEnabled) { + response = handleNewWorkflowState(userState, messageText.trim()); + } else { + response = handleState(userState, messageText.trim()); + } + + // If not in MAIN_MENU or LANGUAGE_SELECTION or EXPIRED, append restart hint + if (response != null && !response.isEmpty() && !response.startsWith("INTERACTIVE:") && !userState.getState().equals("MAIN_MENU") && !userState.getState().equals("LANGUAGE_SELECTION") && !userState.getState().equals("EXPIRED")) { + String rt; + if ("ta".equalsIgnoreCase(userState.getLanguagePreference())) { + rt = "\n\n(உரையாடலை மீண்டும் தொடங்க 0 ஐ அழுத்தவும்)"; + } else if ("hi".equalsIgnoreCase(userState.getLanguagePreference())) { + rt = "\n\n(बातचीत को फिर से शुरू करने के लिए 0 दबाएं)"; + } else { + rt = "\n\n(Press 0 to restart the conversation)"; + } + response += rt; + } + + userStateRepository.save(userState); + + if (response != null && !response.isEmpty()) { + if (response.startsWith("INTERACTIVE:LANG")) { + sendLanguageSelectionMessage(phoneNumberId, from); + } else if (response.startsWith("INTERACTIVE:MAIN_MENU")) { + sendMainMenuInteractiveMessage(phoneNumberId, from, userState.getLanguagePreference()); + } else { + sendMessage(phoneNumberId, from, response); + } + } + + if (!"EXPIRED".equals(userState.getState())) { + scheduleTimeoutTask(from); + } + } finally { + currentPhoneId.remove(); + currentFrom.remove(); + } + } + + private void cancelTimeoutTask(String from) { + java.util.concurrent.ScheduledFuture future = scheduledTasks.remove(from); + if (future != null) { + future.cancel(false); + } + } + + private void scheduleTimeoutTask(String from) { + cancelTimeoutTask(from); + java.util.concurrent.ScheduledFuture future = scheduler.schedule(() -> { + handleTimeout(from); + }, timeoutSeconds, java.util.concurrent.TimeUnit.SECONDS); + scheduledTasks.put(from, future); + } + + private void handleTimeout(String from) { + try { + Optional userStateOpt = userStateRepository.findById(from); + if (userStateOpt.isPresent()) { + UserState userState = userStateOpt.get(); + if ("EXPIRED".equals(userState.getState())) { + return; + } + long elapsed = System.currentTimeMillis() - userState.getLastInteractionTime(); + long timeoutMs = timeoutSeconds * 1000L; + if (elapsed >= (timeoutMs - 1000)) { + logger.info("Session expired due to inactivity for user: {}", from); + userState.setState("EXPIRED"); + userState.setCategory(null); + userState.setSubCategory(null); + userState.setSelectedItem(null); + userState.setPrice(null); + userState.setAddress(null); + userState.setBookingDate(null); + userState.setBookingTime(null); + userState.setMenuPage(0); + userStateRepository.save(userState); + + String lastPhoneId = userState.getLastPhoneNumberId(); + if (lastPhoneId != null) { + String lang = userState.getLanguagePreference(); + String message; + if ("ta".equalsIgnoreCase(lang)) { + message = "உங்கள் முந்தைய உரையாடல் முடிவடைந்துவிட்டது.\n👋 புதிய உரையாடலைத் தொடங்க Hi அல்லது Hello என டைப் செய்யவும்."; + } else { + message = "It looks like your previous conversation has ended.\n👋 To start a new conversation, please type Hi or Hello."; + } + sendMessage(lastPhoneId, from, message); + } else { + logger.warn("Cannot send fallback message for {} because lastPhoneNumberId is null", from); + } + } + } + } catch (Exception e) { + logger.error("Error handling timeout for user " + from, e); + } + } + + @jakarta.annotation.PreDestroy + public void shutdown() { + scheduler.shutdown(); + } + + private String getMessage(String key, UserState state, Object... args) { + java.util.Locale locale = new java.util.Locale(state.getLanguagePreference()); + try { + return messageSource.getMessage(key, args, locale); + } catch (Exception e) { + return key; + } + } + + private String handleState(UserState state, String input) { + if ("NEXT_PAGE".equals(input)) { + state.setMenuPage(state.getMenuPage() + 1); + } + + switch (state.getState()) { + case "LANGUAGE_SELECTION": + return handleLanguageSelection(state, input); + case "MAIN_MENU": + return handleMainMenu(state, input); + case "SERVICES_MENU": + return handleServicesMenu(state, input); + case "NURSING_SERVICES_MENU": + return handleNursingServices(state, input); + case "MALE_NURSE_MENU": + return handleMaleNurse(state, input); + case "FEMALE_NURSE_MENU": + return handleFemaleNurse(state, input); + case "ATTENDER_MENU": + return handleAttender(state, input); + case "FEMALE_ATTENDER_MENU": + return handleFemaleAttender(state, input); + case "PHYSIOTHERAPY_MENU": + return handlePhysiotherapy(state, input); + case "MEDICAL_PROCEDURES_MENU": + return handleMedicalProcedures(state, input); + case "DIGITAL_CONSULTATION_MENU": + return handleDigitalConsultation(state, input); + case "HOME_CARE_PACKAGES_MENU": + return handleHomeCare(state, input); + case "SERVICE_CONFIRM_BOOKING": + return handleServiceConfirmBooking(state, input); + case "SERVICE_ASK_ADDRESS": + return handleServiceAskAddress(state, input); + case "SERVICE_ASK_DATE": + return handleServiceAskDate(state, input); + case "SERVICE_ASK_TIME": + return handleServiceAskTime(state, input); + + case "TESTS_MENU": + return handleTestsMenu(state, input); + case "FEVER_PANEL": + return handleFeverPanel(state, input); + case "HEART_HEALTH": + return handleHeartHealth(state, input); + case "JOINT_BONE": + return handleJointBone(state, input); + case "MASTER_HEALTH": + return handleMasterHealth(state, input); + case "PREGNANCY": + return handlePregnancy(state, input); + case "SUGAR_CONTROL": + return handleSugarControl(state, input); + case "WOMENS_WELLNESS": + return handleWomensWellness(state, input); + case "TEST_CONFIRM_BOOKING": + return handleTestConfirmBooking(state, input); + case "TEST_ASK_COLLECTION_TYPE": + return handleTestAskCollectionType(state, input); + case "TEST_ASK_ADDRESS": + return handleTestAskAddress(state, input); + case "TEST_ASK_DATE": + return handleTestAskDate(state, input); + case "TEST_ASK_TIME": + return handleTestAskTime(state, input); + + case "DOCTOR_MENU": + return handleDoctorMenu(state, input); + case "BOOK_NEW_APPOINTMENT_DEPT": + return handleBookAppointmentDept(state, input); + case "DOCTOR_SELECTION": + return handleDoctorSelection(state, input); + case "DOCTOR_DATE_SELECTION": + return handleDoctorDateSelection(state, input); + case "DOCTOR_TIME_SELECTION": + return handleDoctorTimeSelection(state, input); + case "APPOINTMENT_SUMMARY": + return handleAppointmentSummary(state, input); + case "VIEW_APPOINTMENTS": + return handleViewAppointments(state, input); + case "RESCHEDULE_ASK_ID": + return handleRescheduleAskId(state, input); + case "RESCHEDULE_ASK_TIME": + return handleRescheduleAskTime(state, input); + case "CANCEL_ASK_ID": + return handleCancelAskId(state, input); + case "CANCEL_CONFIRM": + return handleCancelConfirm(state, input); + + case "FINAL_MAIN_MENU": + return handleFinalMainMenu(state, input); + + default: + state.reset(); + return getMainMenuText(state); + } + } + + // ========================================== + // LANGUAGE SELECTION + // ========================================== + private String handleLanguageSelection(UserState state, String input) { + if ("LANG_EN".equals(input) || "1".equals(input) || input.equalsIgnoreCase("English")) { + state.setLanguagePreference("en"); + state.setState("MAIN_MENU"); + return getMainMenuText(state); + } else if ("LANG_TA".equals(input) || "2".equals(input) || input.equals("தமிழ்")) { + state.setLanguagePreference("ta"); + state.setState("MAIN_MENU"); + return getMainMenuText(state); + } else if ("LANG_HI".equals(input) || "3".equals(input) || input.equals("हिंदी") || input.equalsIgnoreCase("Hindi")) { + state.setLanguagePreference("hi"); + state.setState("MAIN_MENU"); + return getMainMenuText(state); + } + return "INTERACTIVE:LANG"; + } + + // ========================================== + // MAIN MENU + // ========================================== + private String handleMainMenu(UserState state, String input) { + switch (input) { + case "1": + case "MENU_SERVICES": + state.setState("SERVICES_MENU"); + return handleServicesMenu(state, ""); // Empty input to trigger menu display + case "2": + case "MENU_TESTS": + state.setState("TESTS_MENU"); + return handleTestsMenu(state, ""); + case "3": + case "MENU_BOOKING": + state.setState("DOCTOR_MENU"); + return handleDoctorMenu(state, ""); + default: + return getMainMenuText(state); + } + } + + private String getMainMenuText(UserState state) { + return "INTERACTIVE:MAIN_MENU"; + } + + // ========================================== + // SERVICES + // ========================================== + private String handleServicesMenu(UserState state, String input) { + switch (input) { + case "1": case "SRV_NURSING": + state.setState("NURSING_SERVICES_MENU"); + sendInteractiveButtons(getMessage("services.nursing.title", state), new String[]{"NSG_MALE", "NSG_FEMALE"}, new String[]{getMessage("services.nursing.opt1", state), getMessage("services.nursing.opt2", state)}); + return null; + case "2": case "SRV_ATTENDER": + state.setState("ATTENDER_MENU"); + sendInteractiveButtons(getMessage("services.attender.title", state), new String[]{"ATT_MALE", "ATT_FEMALE"}, new String[]{getMessage("services.attender.opt1", state), getMessage("services.attender.opt2", state)}); + return null; + case "3": case "SRV_PHYSIO": + state.setState("PHYSIOTHERAPY_MENU"); + state.setSelectedItem("Physiotherapy"); + state.setPrice("₹700"); + sendInteractiveButtons(getMessage("services.physio.title", state), new String[]{"CONFIRM_YES", "CONFIRM_NO"}, new String[]{getMessage("services.physio.yes", state), getMessage("services.physio.no", state)}); + return null; + case "4": case "SRV_MEDICAL_PROC": + state.setState("MEDICAL_PROCEDURES_MENU"); + sendPaginatedButtons(state, getMessage("services.medproc.title", state), new String[]{"PROC_IM", "PROC_IV", "PROC_NEB", "PROC_DRESS", "PROC_CATH"}, new String[]{getMessage("services.medproc.opt1", state), getMessage("services.medproc.opt2", state), getMessage("services.medproc.opt3", state), getMessage("services.medproc.opt4", state), getMessage("services.medproc.opt5", state)}); + return null; + case "5": case "SRV_DIGITAL_CONSULT": + state.setState("DIGITAL_CONSULTATION_MENU"); + state.setSelectedItem("Digital Consultation"); + state.setPrice("₹750"); + sendInteractiveButtons(getMessage("services.digital.title", state), new String[]{"CONFIRM_YES", "CONFIRM_BACK"}, new String[]{getMessage("services.digital.yes", state), getMessage("services.digital.back", state)}); + return null; + case "6": case "SRV_HOME_CARE": + state.setState("HOME_CARE_PACKAGES_MENU"); + sendPaginatedButtons(state, getMessage("services.homecare.title", state), + new String[]{"HC_NURSING", "HC_ATTENDER", "HC_PHYSIO", "HC_MEDICAL_PROC"}, + new String[]{getMessage("services.homecare.opt1", state), getMessage("services.homecare.opt2", state), getMessage("services.homecare.opt3", state), getMessage("services.homecare.opt4", state)}); + return null; + default: + sendPaginatedButtons(state, getMessage("services.main.title", state), new String[]{"SRV_NURSING", "SRV_ATTENDER", "SRV_PHYSIO", "SRV_MEDICAL_PROC", "SRV_DIGITAL_CONSULT", "SRV_HOME_CARE"}, new String[]{getMessage("services.main.opt1", state), getMessage("services.main.opt2", state), getMessage("services.main.opt3", state), getMessage("services.main.opt4", state), getMessage("services.main.opt5", state), getMessage("services.main.opt6", state)}); + return null; + } + } + + private String handleNursingServices(UserState state, String input) { + if ("1".equals(input) || "NSG_MALE".equals(input)) { + state.setState("MALE_NURSE_MENU"); + sendPaginatedButtons(state, "👨‍⚕️ Male Nurse Services", new String[]{"MALE_12H_D", "MALE_24H_D", "MALE_12H_M", "MALE_24H_M"}, new String[]{"12 Hours (Per Day)", "24 Hours (Per Day)", "12 Hours (Month)", "24 Hours (Month)"}); + return null; + } else if ("2".equals(input) || "NSG_FEMALE".equals(input)) { + state.setState("FEMALE_NURSE_MENU"); + sendPaginatedButtons(state, "👩‍⚕️ Female Nurse Services", + new String[]{"FEM_12H_D", "FEM_24H_D", "FEM_12H_M", "FEM_24H_M"}, + new String[]{"12 Hours (Per Day)", "24 Hours (Per Day)", "12 Hours (Month)", "24 Hours (Month)"}); + return null; + } + return "Invalid option. Please use the menu buttons."; + } + + private String handleMaleNurse(UserState state, String input) { + String serviceName = "Male Nurse"; + String price = ""; + switch (input) { + case "1": case "MALE_12H_D": serviceName += " (12 Hours/Day)"; price = "₹1,800"; break; + case "2": case "MALE_24H_D": serviceName += " (24 Hours/Day)"; price = "₹3,300"; break; + case "3": case "MALE_12H_M": serviceName += " (12 Hours/Month)"; price = "₹54,000"; break; + case "4": case "MALE_24H_M": serviceName += " (24 Hours/Month)"; price = "₹99,000"; break; + default: return "Invalid option."; + } + state.setSelectedItem(serviceName); + state.setPrice(price); + state.setState("SERVICE_CONFIRM_BOOKING"); + sendInteractiveButtons(serviceName + "\nPrice:\n" + price + "\nWould you like to book this service?", new String[]{"BOOK_CONFIRM", "BOOK_BACK"}, new String[]{"Confirm Booking", "Back"}); + return null; + } + + private String handleFemaleNurse(UserState state, String input) { + String serviceName = "Female Nurse"; + String price = ""; + switch (input) { + case "1": case "FEM_12H_D": serviceName += " (12 Hours/Day)"; price = "₹1,500"; break; + case "2": case "FEM_24H_D": serviceName += " (24 Hours/Day)"; price = "₹3,000"; break; + case "3": case "FEM_12H_M": serviceName += " (12 Hours/Month)"; price = "₹45,000"; break; + case "4": case "FEM_24H_M": serviceName += " (24 Hours/Month)"; price = "₹90,000"; break; + default: return "Invalid option."; + } + state.setSelectedItem(serviceName); + state.setPrice(price); + state.setState("SERVICE_CONFIRM_BOOKING"); + sendInteractiveButtons(serviceName + "\nPrice:\n" + price + "\nWould you like to book this service?", new String[]{"BOOK_CONFIRM", "BOOK_BACK"}, new String[]{"Confirm Booking", "Back"}); + return null; + } + + private String handleAttender(UserState state, String input) { + if ("1".equals(input) || "ATT_MALE".equals(input)) { + state.setSelectedItem("Male Attender"); + state.setPrice("₹1,200"); + state.setState("SERVICE_CONFIRM_BOOKING"); + sendInteractiveButtons("Male Attender\nPrice: ₹1,200\nWould you like to book this service?", new String[]{"BOOK_CONFIRM", "BOOK_BACK"}, new String[]{"Confirm Booking", "Back"}); + return null; + } else if ("2".equals(input) || "ATT_FEMALE".equals(input)) { + state.setState("FEMALE_ATTENDER_MENU"); + sendPaginatedButtons(state, "👩‍⚕️ Female Attender", + new String[]{"F_ATT_12H_D", "F_ATT_24H_D", "F_ATT_12H_M", "F_ATT_24H_M"}, + new String[]{"12 Hours (Per Day)", "24 Hours (Per Day)", "12 Hours (Month)", "24 Hours (Month)"}); + return null; + } + return "Invalid option. Please use the menu buttons."; + } + + private String handleFemaleAttender(UserState state, String input) { + String serviceName = "Female Attender"; + String price = ""; + switch (input) { + case "1": case "F_ATT_12H_D": serviceName += " (12 Hours)"; price = "₹1,200"; break; + case "2": case "F_ATT_24H_D": serviceName += " (24 Hours)"; price = "₹1,700"; break; + case "3": case "F_ATT_12H_M": serviceName += " (12 Hours/Month)"; price = "₹36,000"; break; + case "4": case "F_ATT_24H_M": serviceName += " (24 Hours/Month)"; price = "₹51,000"; break; + default: return "Invalid option."; + } + state.setSelectedItem(serviceName); + state.setPrice(price); + state.setState("SERVICE_CONFIRM_BOOKING"); + sendInteractiveButtons(serviceName + "\nPrice:\n" + price + "\nWould you like to book this service?", new String[]{"BOOK_CONFIRM", "BOOK_BACK"}, new String[]{"Confirm Booking", "Back"}); + return null; + } + + private String handlePhysiotherapy(UserState state, String input) { + if ("1".equals(input) || input.equalsIgnoreCase("Yes") || "CONFIRM_YES".equals(input)) { + state.setState("SERVICE_ASK_ADDRESS"); + return "✅ Booking Confirmed\nService: " + state.getSelectedItem() + "\nAmount: " + state.getPrice() + "\n\nPlease share:\n📍 Service Address\nor\n📍 Live Location"; + } else { + state.reset(); + return getMainMenuText(state); + } + } + + private String handleMedicalProcedures(UserState state, String input) { + String serviceName = ""; + String price = ""; + switch (input) { + case "1": case "PROC_IM": serviceName = "IM Injection"; price = "₹400"; break; + case "2": case "PROC_IV": serviceName = "IV Injection"; price = "₹500"; break; + case "3": case "PROC_NEB": serviceName = "Nebulization"; price = "₹1,000"; break; + case "4": case "PROC_DRESS": serviceName = "Dressing Charges"; price = "₹750"; break; + case "5": case "PROC_CATH": serviceName = "Catheterization"; price = "₹1,500"; break; + default: return "Invalid option."; + } + state.setSelectedItem(serviceName); + state.setPrice(price); + state.setState("SERVICE_CONFIRM_BOOKING"); + sendInteractiveButtons(serviceName + "\n\nPrice:\n" + price + "\n\nWould you like to continue?", new String[]{"BOOK_CONFIRM", "BOOK_CANCEL"}, new String[]{"Confirm", "Cancel"}); + return null; + } + + private String handleDigitalConsultation(UserState state, String input) { + if ("1".equals(input) || input.equalsIgnoreCase("Yes") || "CONFIRM_YES".equals(input)) { + state.setState("SERVICE_ASK_ADDRESS"); + return getMessage("booking.service.confirm", state, state.getSelectedItem(), state.getPrice()); + } else { + state.reset(); + return getMainMenuText(state); + } + } + + private String handleHomeCare(UserState state, String input) { + String service = input; + switch(input) { + case "HC_NURSING": service = "Nursing Care"; break; + case "HC_ATTENDER": service = "Attender Care"; break; + case "HC_PHYSIO": service = "Physiotherapy"; break; + case "HC_MEDICAL_PROC": service = "Medical Procedures"; break; + } + state.setSelectedItem("Home Care: " + service); + state.setPrice("Varies"); + state.setState("SERVICE_CONFIRM_BOOKING"); + sendInteractiveButtons("Home Care Package selected.\nWould you like to continue?", new String[]{"BOOK_CONFIRM", "BOOK_CANCEL"}, new String[]{"Confirm", "Cancel"}); + return null; + } + + private String handleServiceConfirmBooking(UserState state, String input) { + if ("1".equals(input) || "BOOK_CONFIRM".equals(input) || input.equalsIgnoreCase("Confirm Booking") || input.equalsIgnoreCase("Confirm")) { + state.setState("SERVICE_ASK_ADDRESS"); + return getMessage("booking.service.confirm", state, state.getSelectedItem(), state.getPrice()); + } else { + state.reset(); + return getMainMenuText(state); + } + } + + private String handleServiceAskAddress(UserState state, String input) { + state.setAddress(input); + state.setState("SERVICE_ASK_DATE"); + return getMessage("booking.ask_date", state); + } + + private String handleServiceAskDate(UserState state, String input) { + state.setBookingDate(input); + state.setState("SERVICE_ASK_TIME"); + sendPaginatedButtons(state, getMessage("booking.ask_time.title", state), + new String[]{"TIME_9_1", "TIME_1_5", "TIME_5_9"}, + new String[]{getMessage("booking.ask_time.opt1", state), getMessage("booking.ask_time.opt2", state), getMessage("booking.ask_time.opt3", state)}); + return null; + } + + private String handleServiceAskTime(UserState state, String input) { + String time = input; + switch(input) { + case "TIME_9_1": time = "9 AM - 1 PM"; break; + case "TIME_1_5": time = "1 PM - 5 PM"; break; + case "TIME_5_9": time = "5 PM - 9 PM"; break; + } + state.setBookingTime(time); + + // Save to DB + ServiceBooking booking = new ServiceBooking(); + booking.setPhoneNumber(state.getPhoneNumber()); + String bookingId = "RMC-HC-" + (10000 + new Random().nextInt(90000)); + booking.setBookingId(bookingId); + booking.setServiceName(state.getSelectedItem()); + booking.setPrice(state.getPrice()); + booking.setAddress(state.getAddress()); + booking.setBookingDate(state.getBookingDate()); + booking.setBookingTime(state.getBookingTime()); + booking.setPatientId(state.getPatientId()); + booking.setStatus("CONFIRMED"); + serviceBookingRepository.save(booking); + + + state.setState("FINAL_MAIN_MENU"); + return getMessage("booking.service.success", state, bookingId, state.getPatientId()) + "\n\n" + getFinalMainMenuText(state); + } + + + // ========================================== + // TESTS MENU + // ========================================== + private String handleTestsMenu(UserState state, String input) { + switch(input) { + case "1": case "TEST_FEVER": + state.setState("FEVER_PANEL"); + sendPaginatedButtons(state, getMessage("tests.fever.title", state), new String[]{"FEVER_ULTRA", "FEVER_CBC", "FEVER_IGE", "FEVER_LFT", "FEVER_STOOL"}, new String[]{getMessage("tests.fever.opt1", state), getMessage("tests.fever.opt2", state), getMessage("tests.fever.opt3", state), getMessage("tests.fever.opt4", state), getMessage("tests.fever.opt5", state)}); + return null; + case "2": case "TEST_HEART": + state.setState("HEART_HEALTH"); + sendPaginatedButtons(state, getMessage("tests.heart.title", state), + new String[]{"HRT_PLUS", "HRT_AMAYA_INT", "HRT_AMAYA", "HRT_HEALTHY", "HRT_BASIC", "HRT_MASTER", "HRT_HYPER"}, + new String[]{getMessage("tests.heart.opt1", state), getMessage("tests.heart.opt2", state), getMessage("tests.heart.opt3", state), getMessage("tests.heart.opt4", state), getMessage("tests.heart.opt5", state), getMessage("tests.heart.opt6", state), getMessage("tests.heart.opt7", state)}); + return null; + case "3": case "TEST_JOINT": + state.setState("JOINT_BONE"); + sendInteractiveButtons(getMessage("tests.joint.title", state), new String[]{"JOINT_PLUS", "JOINT_ARTH"}, new String[]{getMessage("tests.joint.opt1", state), getMessage("tests.joint.opt2", state)}); + return null; + case "4": case "TEST_MASTER": + state.setState("MASTER_HEALTH"); + sendPaginatedButtons(state, getMessage("tests.master.title", state), + new String[]{"MH_BASIC", "MH_EXEC", "MH_PREMIUM"}, + new String[]{getMessage("tests.master.opt1", state), getMessage("tests.master.opt2", state), getMessage("tests.master.opt3", state)}); + return null; + case "5": case "TEST_PREGNANCY": + state.setState("PREGNANCY"); + sendInteractiveButtons(getMessage("tests.preg.title", state), new String[]{"PREG_BASIC", "PREG_ADV"}, new String[]{getMessage("tests.preg.opt1", state), getMessage("tests.preg.opt2", state)}); + return null; + case "6": case "TEST_SUGAR": + state.setState("SUGAR_CONTROL"); + sendPaginatedButtons(state, getMessage("tests.sugar.title", state), new String[]{"SUGAR_BASIC", "SUGAR_PLUS", "SUGAR_MONITOR", "SUGAR_HBA1C"}, new String[]{getMessage("tests.sugar.opt1", state), getMessage("tests.sugar.opt2", state), getMessage("tests.sugar.opt3", state), getMessage("tests.sugar.opt4", state)}); + return null; + case "7": case "TEST_WOMEN": + state.setState("WOMENS_WELLNESS"); + sendInteractiveButtons(getMessage("tests.women.title", state), new String[]{"WOM_BASIC", "WOM_ADV"}, new String[]{getMessage("tests.women.opt1", state), getMessage("tests.women.opt2", state)}); + return null; + default: + sendPaginatedButtons(state, getMessage("tests.main.title", state), new String[]{"TEST_FEVER", "TEST_HEART", "TEST_JOINT", "TEST_MASTER", "TEST_PREGNANCY", "TEST_SUGAR", "TEST_WOMEN"}, new String[]{getMessage("tests.main.opt1", state), getMessage("tests.main.opt2", state), getMessage("tests.main.opt3", state), getMessage("tests.main.opt4", state), getMessage("tests.main.opt5", state), getMessage("tests.main.opt6", state), getMessage("tests.main.opt7", state)}); + return null; + } + } + + private String handleFeverPanel(UserState state, String input) { + String test = "Fever Ultra Panel"; + String price = "₹1,999"; + switch(input) { + case "1": case "FEVER_CBC": test = "CBC"; price = "₹300"; break; + case "2": case "FEVER_IGE": test = "IgE Total Serum"; price = "₹400"; break; + case "3": case "FEVER_LFT": test = "Liver Function Test (LFT)"; price = "₹500"; break; + case "4": case "FEVER_STOOL": test = "Stool for Occult Blood"; price = "₹250"; break; + case "FEVER_ULTRA": break; + default: return "Invalid option."; + } + state.setSelectedItem(test); + state.setPrice(price); + state.setState("TEST_CONFIRM_BOOKING"); + sendInteractiveButtons("Selected Package\n\n" + test + "\nPrice:\n" + price + "\nWould you like to continue?", new String[]{"TEST_BOOK", "TEST_BACK"}, new String[]{"Book Now", "Back"}); + return null; + } + + private String handleHeartHealth(UserState state, String input) { + String test = ""; String price = ""; + switch(input) { + case "1": case "HRT_PLUS": test = "Heart Health Plus"; price = "₹3,300"; break; + case "2": case "HRT_AMAYA_INT": test = "Amaya International Cardiac"; price = "₹2,121"; break; + case "3": case "HRT_AMAYA": test = "Cardiac Package - Amaya"; price = "₹3,360"; break; + case "4": case "HRT_HEALTHY": test = "Healthy Heart Basic"; price = "₹799"; break; + case "5": case "HRT_BASIC": test = "Heart Basic"; price = "₹599"; break; + case "6": case "HRT_MASTER": test = "Heart Master"; price = "₹1,399"; break; + case "7": case "HRT_HYPER": test = "Hypertension Comprehensive Package"; price = "₹2,999"; break; + default: return "Invalid option."; + } + state.setSelectedItem(test); + state.setPrice(price); + state.setState("TEST_CONFIRM_BOOKING"); + sendInteractiveButtons("Selected Package\n\n" + test + "\nPrice:\n" + price + "\nWould you like to continue?", new String[]{"TEST_BOOK", "TEST_BACK"}, new String[]{"Book Now", "Back"}); + return null; + } + + private String handleJointBone(UserState state, String input) { + String test = ""; String price = ""; + switch(input) { + case "1": case "JOINT_PLUS": test = "Joint Care Plus"; price = "₹1,599"; break; + case "2": case "JOINT_ARTH": test = "Arthritis Package"; price = "₹799"; break; + default: return "Invalid option."; + } + state.setSelectedItem(test); + state.setPrice(price); + state.setState("TEST_CONFIRM_BOOKING"); + sendInteractiveButtons("Selected Package\n\n" + test + "\nPrice:\n" + price + "\nWould you like to continue?", new String[]{"TEST_BOOK", "TEST_BACK"}, new String[]{"Book Now", "Back"}); + return null; + } + + private String handleMasterHealth(UserState state, String input) { + String test = ""; String price = "₹2,500"; + switch(input) { + case "1": case "MH_BASIC": test = "Master Health Basic"; break; + case "2": case "MH_EXEC": test = "Master Health Executive"; break; + case "3": case "MH_PREMIUM": test = "Master Health Premium"; break; + default: return "Invalid option."; + } + state.setSelectedItem(test); + state.setPrice(price); + state.setState("TEST_CONFIRM_BOOKING"); + sendInteractiveButtons("Selected Package\n\n" + test + "\nPrice:\n" + price + "\nWould you like to continue?", new String[]{"TEST_BOOK", "TEST_BACK"}, new String[]{"Book Now", "Back"}); + return null; + } + + private String handlePregnancy(UserState state, String input) { + String test = ""; String price = "₹1,500"; + switch(input) { + case "1": case "PREG_BASIC": test = "Pregnancy Basic"; break; + case "2": case "PREG_ADV": test = "Pregnancy Advanced"; break; + default: return "Invalid option."; + } + state.setSelectedItem(test); + state.setPrice(price); + state.setState("TEST_CONFIRM_BOOKING"); + sendInteractiveButtons("Selected Package\n\n" + test + "\nPrice:\n" + price + "\nWould you like to continue?", new String[]{"TEST_BOOK", "TEST_BACK"}, new String[]{"Book Now", "Back"}); + return null; + } + + private String handleSugarControl(UserState state, String input) { + String test = ""; String price = "₹999"; + switch(input) { + case "1": case "SUGAR_BASIC": test = "Sugar Control Basic"; break; + case "2": case "SUGAR_PLUS": test = "Sugar Control Plus"; break; + case "3": case "SUGAR_MONITOR": test = "Diabetes Monitoring"; break; + case "4": case "SUGAR_HBA1C": test = "HbA1c Comprehensive"; break; + default: return "Invalid option."; + } + state.setSelectedItem(test); + state.setPrice(price); + state.setState("TEST_CONFIRM_BOOKING"); + sendInteractiveButtons(getMessage("booking.test.confirm_pkg", state, test, price), new String[]{"TEST_BOOK", "TEST_BACK"}, new String[]{getMessage("booking.test.btn_book", state), getMessage("booking.test.btn_back", state)}); + return null; + } + + private String handleWomensWellness(UserState state, String input) { + String test = ""; String price = "₹1,200"; + switch(input) { + case "1": case "WOM_BASIC": test = "Women's Wellness Basic"; break; + case "2": case "WOM_ADV": test = "Women's Wellness Advanced"; break; + default: return "Invalid option."; + } + state.setSelectedItem(test); + state.setPrice(price); + state.setState("TEST_CONFIRM_BOOKING"); + sendInteractiveButtons(getMessage("booking.test.confirm_pkg", state, test, price), new String[]{"TEST_BOOK", "TEST_BACK"}, new String[]{getMessage("booking.test.btn_book", state), getMessage("booking.test.btn_back", state)}); + return null; + } + + private String handleTestConfirmBooking(UserState state, String input) { + if ("1".equals(input) || "TEST_BOOK".equals(input) || input.equalsIgnoreCase("Book Now")) { + state.setState("TEST_ASK_COLLECTION_TYPE"); + sendInteractiveButtons(getMessage("booking.test.ask_coll", state), new String[]{"COLL_HOME", "COLL_VISIT"}, new String[]{getMessage("booking.test.coll_home", state), getMessage("booking.test.coll_visit", state)}); + return null; + } else { + state.reset(); + return getMainMenuText(state); + } + } + + private String handleTestAskCollectionType(UserState state, String input) { + if ("1".equals(input) || "COLL_HOME".equals(input)) { + state.setSubCategory("Home Sample Collection"); + state.setState("TEST_ASK_ADDRESS"); + return getMessage("booking.ask_address", state); + } else if ("2".equals(input) || "COLL_VISIT".equals(input)) { + state.setSubCategory("Visit Laboratory"); + state.setAddress("Rajalakshmi Medical College Laboratory"); + state.setState("TEST_ASK_DATE"); + return getMessage("booking.test.ask_date", state); + } + return getMessage("invalid.option", state); + } + + private String handleTestAskAddress(UserState state, String input) { + state.setAddress(input); + state.setState("TEST_ASK_DATE"); + return getMessage("booking.test.ask_date", state); + } + + private String handleTestAskDate(UserState state, String input) { + state.setBookingDate(input); + state.setState("TEST_ASK_TIME"); + sendPaginatedButtons(state, getMessage("booking.test.ask_time.title", state), new String[]{"SLOT_7AM", "SLOT_9AM", "SLOT_11AM"}, new String[]{getMessage("booking.test.opt1", state), getMessage("booking.test.opt2", state), getMessage("booking.test.opt3", state)}); + return null; + } + + private String handleTestAskTime(UserState state, String input) { + String time = input; + switch(input) { + case "1": case "SLOT_7AM": time = "7 AM – 9 AM"; break; + case "2": case "SLOT_9AM": time = "9 AM – 11 AM"; break; + case "3": case "SLOT_11AM": time = "11 AM – 1 PM"; break; + } + state.setBookingTime(time); + + // Save DB + TestBooking booking = new TestBooking(); + booking.setPhoneNumber(state.getPhoneNumber()); + String bookingId = "LAB" + (1000000 + new Random().nextInt(9000000)); + booking.setBookingId(bookingId); + booking.setPatientId(state.getPatientId()); + booking.setPackageName(state.getSelectedItem()); + booking.setPrice(state.getPrice()); + booking.setCollectionType(state.getSubCategory()); + booking.setAddress(state.getAddress()); + booking.setBookingDate(state.getBookingDate()); + booking.setBookingTime(state.getBookingTime()); + booking.setStatus("CONFIRMED"); + testBookingRepository.save(booking); + + state.setState("FINAL_MAIN_MENU"); + return getMessage("booking.test.success", state, bookingId, state.getPatientId(), state.getSelectedItem(), state.getPrice(), state.getSubCategory(), state.getBookingDate(), state.getBookingTime()) + "\n\n" + getFinalMainMenuText(state); + } + + // ========================================== + private String handleDoctorMenu(UserState state, String input) { + switch(input) { + case "1": case "DOC_NEW_APP": + state.setState("BOOK_NEW_APPOINTMENT_DEPT"); + sendPaginatedButtons(state, getMessage("doc.dept.title", state), new String[]{"DEPT_GEN_MED", "DEPT_CARDIO", "DEPT_ORTHO", "DEPT_NEURO", "DEPT_PEDS", "DEPT_GYNO", "DEPT_DERM", "DEPT_ENT", "DEPT_OPHTH"}, new String[]{getMessage("doc.dept.opt1", state), getMessage("doc.dept.opt2", state), getMessage("doc.dept.opt3", state), getMessage("doc.dept.opt4", state), getMessage("doc.dept.opt5", state), getMessage("doc.dept.opt6", state), getMessage("doc.dept.opt7", state), getMessage("doc.dept.opt8", state), getMessage("doc.dept.opt9", state)}); + return null; + case "2": case "DOC_VIEW_APP": + return listAppointments(state); + case "3": case "DOC_RESCHEDULE": + state.setState("RESCHEDULE_ASK_ID"); + return getMessage("doc.reschedule.ask", state); + case "4": case "DOC_CANCEL": + state.setState("CANCEL_ASK_ID"); + return getMessage("doc.cancel.ask", state); + default: + sendPaginatedButtons(state, getMessage("doc.main.title", state), new String[]{"DOC_NEW_APP", "DOC_VIEW_APP", "DOC_RESCHEDULE", "DOC_CANCEL"}, new String[]{getMessage("doc.main.opt1", state), getMessage("doc.main.opt2", state), getMessage("doc.main.opt3", state), getMessage("doc.main.opt4", state)}); + return null; + } + } + + private String handleBookAppointmentDept(UserState state, String input) { + String dept = ""; + switch(input) { + case "1": case "DEPT_GEN_MED": dept = "General Medicine"; break; + case "2": case "DEPT_CARDIO": dept = "Cardiology"; break; + case "3": case "DEPT_ORTHO": dept = "Orthopedics"; break; + case "4": case "DEPT_NEURO": dept = "Neurology"; break; + case "5": case "DEPT_PEDS": dept = "Pediatrics"; break; + case "6": case "DEPT_GYNO": dept = "Gynecology"; break; + case "7": case "DEPT_DERM": dept = "Dermatology"; break; + case "8": case "DEPT_ENT": dept = "ENT"; break; + case "9": case "DEPT_OPHTH": dept = "Ophthalmology"; break; + default: return "Invalid option."; + } + state.setCategory(dept); + state.setState("DOCTOR_SELECTION"); + sendPaginatedButtons(state, dept + "\n\n" + getMessage("doc.sel.title", state), new String[]{"DOC_RAJESH", "DOC_PRIYA", "DOC_ARAVIND"}, new String[]{"Dr. Rajesh Kumar", "Dr. Priya Sharma", "Dr. Aravind"}); + return null; + } + + private String handleDoctorSelection(UserState state, String input) { + String doc = ""; + switch(input) { + case "1": case "DOC_RAJESH": doc = "Dr. Rajesh Kumar"; break; + case "2": case "DOC_PRIYA": doc = "Dr. Priya Sharma"; break; + case "3": case "DOC_ARAVIND": doc = "Dr. Aravind"; break; + default: return "Invalid option."; + } + state.setSelectedItem(doc); + state.setState("DOCTOR_DATE_SELECTION"); + sendPaginatedButtons(state, doc + "\n" + getMessage("doc.date.title", state), new String[]{"DATE_25_JUL", "DATE_26_JUL", "DATE_27_JUL"}, new String[]{"25 July 2026", "26 July 2026", "27 July 2026"}); + return null; + } + + private String handleDoctorDateSelection(UserState state, String input) { + String date = ""; + switch(input) { + case "1": case "DATE_25_JUL": date = "25 July 2026"; break; + case "2": case "DATE_26_JUL": date = "26 July 2026"; break; + case "3": case "DATE_27_JUL": date = "27 July 2026"; break; + default: date = input; break; + } + state.setBookingDate(date); + state.setState("DOCTOR_TIME_SELECTION"); + sendPaginatedButtons(state, getMessage("doc.time.title", state), new String[]{"TIME_9AM", "TIME_10AM", "TIME_11_30AM", "TIME_2PM", "TIME_4PM"}, new String[]{"09:00 AM", "10:00 AM", "11:30 AM", "02:00 PM", "04:00 PM"}); + return null; + } + + private String handleDoctorTimeSelection(UserState state, String input) { + String time = ""; + switch(input) { + case "1": case "TIME_9AM": time = "09:00 AM"; break; + case "2": case "TIME_10AM": time = "10:00 AM"; break; + case "3": case "TIME_11_30AM": time = "11:30 AM"; break; + case "4": case "TIME_2PM": time = "02:00 PM"; break; + case "5": case "TIME_4PM": time = "04:00 PM"; break; + default: time = input; break; + } + state.setBookingTime(time); + state.setPrice("₹600"); + state.setState("APPOINTMENT_SUMMARY"); + sendPaginatedButtons(state, getMessage("doc.summary.title", state) + "\n\nDoctor:\n" + state.getSelectedItem() + "\n\n🏥 Department:\n" + state.getCategory() + "\n\n📅 Date:\n" + state.getBookingDate() + "\n\n🕑 Time:\n" + state.getBookingTime() + "\n\nConsultation Fee:\n" + state.getPrice(), new String[]{"APT_CONFIRM", "APT_CHANGE_TIME", "APT_CANCEL"}, new String[]{getMessage("doc.summary.confirm", state), getMessage("doc.summary.change", state), getMessage("doc.summary.cancel", state)}); + return null; + } + + private String handleAppointmentSummary(UserState state, String input) { + if ("1".equals(input) || "APT_CONFIRM".equals(input)) { + // Save DB + Appointment app = new Appointment(); + app.setPhoneNumber(state.getPhoneNumber()); + String appId = "APT" + (100000 + new Random().nextInt(900000)); + app.setAppointmentId(appId); + app.setDepartment(state.getCategory()); + app.setDoctor(state.getSelectedItem()); + app.setAppointmentDate(state.getBookingDate()); + app.setAppointmentTime(state.getBookingTime()); + app.setFee(state.getPrice()); + app.setPatientId(state.getPatientId()); + app.setStatus("Confirmed"); + appointmentRepository.save(app); + + state.setState("FINAL_MAIN_MENU"); + return getMessage("doc.booking.success", state, appId, state.getPatientId(), state.getSelectedItem(), state.getCategory(), state.getBookingDate(), state.getBookingTime()) + "\n\n" + getFinalMainMenuText(state); + } else if ("2".equals(input) || "APT_CHANGE_TIME".equals(input)) { + state.setState("DOCTOR_TIME_SELECTION"); + sendPaginatedButtons(state, getMessage("doc.time.title", state), new String[]{"TIME_9AM", "TIME_10AM", "TIME_11_30AM", "TIME_2PM", "TIME_4PM"}, new String[]{"09:00 AM", "10:00 AM", "11:30 AM", "02:00 PM", "04:00 PM"}); + return null; + } else { + state.reset(); + return getMainMenuText(state); + } + } + + private String listAppointments(UserState state) { + List list = appointmentRepository.findByPhoneNumber(state.getPhoneNumber()); + if (list.isEmpty()) { + state.reset(); + return getMessage("doc.no_apps", state) + "\n\n" + getMainMenuText(state); + } + StringBuilder sb = new StringBuilder(getMessage("doc.view_apps.title", state) + "\n\n"); + for (Appointment app : list) { + sb.append(getMessage("doc.view_apps.item", state, app.getAppointmentId(), app.getDoctor(), app.getDepartment(), app.getAppointmentDate(), app.getAppointmentTime(), app.getStatus())).append("\n\n"); + } + state.setState("VIEW_APPOINTMENTS"); + sendPaginatedButtons(state, sb.toString(), new String[]{"VIEW_CANCEL", "VIEW_RESCHEDULE", "VIEW_MAIN_MENU"}, new String[]{getMessage("doc.view_apps.opt1", state), getMessage("doc.view_apps.opt2", state), getMessage("doc.view_apps.opt3", state)}); + return null; + } + + private String handleViewAppointments(UserState state, String input) { + if ("1".equals(input) || "VIEW_CANCEL".equals(input)) { + state.setState("CANCEL_ASK_ID"); + return getMessage("doc.cancel.ask", state); + } else if ("2".equals(input) || "VIEW_RESCHEDULE".equals(input)) { + state.setState("RESCHEDULE_ASK_ID"); + return getMessage("doc.reschedule.ask", state); + } else { + state.setState("FINAL_MAIN_MENU"); + return getFinalMainMenuText(state); + } + } + + private String handleRescheduleAskId(UserState state, String input) { + Optional optApp = appointmentRepository.findByAppointmentId(input); + if (optApp.isPresent()) { + state.setSubCategory(input); // store ID + state.setState("RESCHEDULE_ASK_TIME"); + sendPaginatedButtons(state, getMessage("doc.reschedule.time.title", state), new String[]{"RESCHED_9AM", "RESCHED_11AM", "RESCHED_3PM"}, new String[]{"09:00 AM", "11:00 AM", "03:00 PM"}); + return null; + } + return getMessage("doc.invalid_id", state); + } + + private String handleRescheduleAskTime(UserState state, String input) { + String time = ""; + switch(input) { + case "1": case "RESCHED_9AM": time = "09:00 AM"; break; + case "2": case "RESCHED_11AM": time = "11:00 AM"; break; + case "3": case "RESCHED_3PM": time = "03:00 PM"; break; + default: time = input; break; + } + Optional optApp = appointmentRepository.findByAppointmentId(state.getSubCategory()); + if (optApp.isPresent()) { + Appointment app = optApp.get(); + app.setAppointmentTime(time); + appointmentRepository.save(app); + state.setState("FINAL_MAIN_MENU"); + return getMessage("doc.reschedule.success", state, app.getDoctor(), time, app.getAppointmentDate()) + "\n\n" + getFinalMainMenuText(state); + } + return getMessage("error.generic", state); + } + + private String handleCancelAskId(UserState state, String input) { + Optional optApp = appointmentRepository.findByAppointmentId(input); + if (optApp.isPresent()) { + state.setSubCategory(input); // store ID + state.setState("CANCEL_CONFIRM"); + sendInteractiveButtons(getMessage("doc.cancel.confirm_msg", state), new String[]{"CANCEL_YES", "CANCEL_NO"}, new String[]{getMessage("doc.cancel.yes", state), getMessage("doc.cancel.no", state)}); + return null; + } + return getMessage("doc.invalid_id", state); + } + + private String handleCancelConfirm(UserState state, String input) { + if ("1".equals(input) || input.equalsIgnoreCase("Yes") || "CANCEL_YES".equals(input)) { + Optional optApp = appointmentRepository.findByAppointmentId(state.getSubCategory()); + if (optApp.isPresent()) { + Appointment app = optApp.get(); + app.setStatus("Cancelled"); + appointmentRepository.save(app); + state.setState("FINAL_MAIN_MENU"); + return "✅ Appointment Cancelled Successfully.\nIf applicable, your refund will be processed within 5–7 working days.\nThank you.\n\n" + getFinalMainMenuText(state); + } + } + state.setState("FINAL_MAIN_MENU"); + return "Cancellation aborted.\n\n" + getFinalMainMenuText(state); + } + + // ========================================== + // FINAL MAIN MENU + // ========================================== + private String getFinalMainMenuText(UserState state) { + return "Rajalakshmi Medical College\n\nWhat would you like to do next?\nReply 0 to restart the conversation."; + } + + private String handleFinalMainMenu(UserState state, String input) { + return handleMainMenu(state, input); + } + + + // ========================================== + // SENDER FUNCTION + // ========================================== + private void sendLanguageSelectionMessage(String phoneNumberId, String recipientPhoneNumber) { + String url = "https://graph.facebook.com/v20.0/" + phoneNumberId + "/messages"; + + try { + ObjectNode requestBody = objectMapper.createObjectNode(); + requestBody.put("messaging_product", "whatsapp"); + requestBody.put("recipient_type", "individual"); + requestBody.put("to", recipientPhoneNumber); + requestBody.put("type", "interactive"); + + ObjectNode interactiveNode = objectMapper.createObjectNode(); + interactiveNode.put("type", "button"); + + ObjectNode bodyNode = objectMapper.createObjectNode(); + bodyNode.put("text", "Welcome to RMC Hospital! Please select your preferred language. / RMC மருத்துவமனைக்கு உங்களை வரவேற்கிறோம்! உங்கள் மொழியைத் தேர்ந்தெடுக்கவும். / आरएमसी अस्पताल में आपका स्वागत है! कृपया अपनी पसंदीदा भाषा चुनें।"); + interactiveNode.set("body", bodyNode); + + ObjectNode actionNode = objectMapper.createObjectNode(); + com.fasterxml.jackson.databind.node.ArrayNode buttonsArray = actionNode.putArray("buttons"); + + ObjectNode btn1 = objectMapper.createObjectNode(); + btn1.put("type", "reply"); + ObjectNode reply1 = objectMapper.createObjectNode(); + reply1.put("id", "LANG_EN"); + reply1.put("title", "English"); + btn1.set("reply", reply1); + buttonsArray.add(btn1); + + ObjectNode btn2 = objectMapper.createObjectNode(); + btn2.put("type", "reply"); + ObjectNode reply2 = objectMapper.createObjectNode(); + reply2.put("id", "LANG_TA"); + reply2.put("title", "தமிழ்"); + btn2.set("reply", reply2); + buttonsArray.add(btn2); + + ObjectNode btn3 = objectMapper.createObjectNode(); + btn3.put("type", "reply"); + ObjectNode reply3 = objectMapper.createObjectNode(); + reply3.put("id", "LANG_HI"); + reply3.put("title", "हिंदी"); + btn3.set("reply", reply3); + buttonsArray.add(btn3); + + interactiveNode.set("action", actionNode); + requestBody.set("interactive", interactiveNode); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(accessToken); + + HttpEntity entity = new HttpEntity<>(requestBody.toString(), headers); + logger.info("Sending interactive message to {}", recipientPhoneNumber); + restTemplate.postForEntity(url, entity, String.class); + } catch (org.springframework.web.client.HttpStatusCodeException e) { + logger.error("Error sending interactive message. Status: {}, Response: {}", e.getStatusCode(), e.getResponseBodyAsString(), e); + } catch (Exception e) { + logger.error("Error sending interactive message", e); + } + } + + private void sendMainMenuInteractiveMessage(String phoneNumberId, String recipientPhoneNumber, String languagePref) { + String url = "https://graph.facebook.com/v20.0/" + phoneNumberId + "/messages"; + + try { + java.util.Locale locale = new java.util.Locale(languagePref != null ? languagePref : "en"); + String bodyText = messageSource.getMessage("welcome.body", null, locale); + String btn1Title = messageSource.getMessage("menu.btn.services", null, locale); + String btn2Title = messageSource.getMessage("menu.btn.tests", null, locale); + String btn3Title = messageSource.getMessage("menu.btn.booking", null, locale); + + ObjectNode requestBody = objectMapper.createObjectNode(); + requestBody.put("messaging_product", "whatsapp"); + requestBody.put("recipient_type", "individual"); + requestBody.put("to", recipientPhoneNumber); + requestBody.put("type", "interactive"); + + ObjectNode interactiveNode = objectMapper.createObjectNode(); + interactiveNode.put("type", "button"); + + ObjectNode bodyNode = objectMapper.createObjectNode(); + bodyNode.put("text", bodyText); + interactiveNode.set("body", bodyNode); + + ObjectNode actionNode = objectMapper.createObjectNode(); + com.fasterxml.jackson.databind.node.ArrayNode buttonsArray = actionNode.putArray("buttons"); + + ObjectNode btn1 = objectMapper.createObjectNode(); + btn1.put("type", "reply"); + ObjectNode reply1 = objectMapper.createObjectNode(); + reply1.put("id", "MENU_SERVICES"); + reply1.put("title", btn1Title); + btn1.set("reply", reply1); + buttonsArray.add(btn1); + + ObjectNode btn2 = objectMapper.createObjectNode(); + btn2.put("type", "reply"); + ObjectNode reply2 = objectMapper.createObjectNode(); + reply2.put("id", "MENU_TESTS"); + reply2.put("title", btn2Title); + btn2.set("reply", reply2); + buttonsArray.add(btn2); + + ObjectNode btn3 = objectMapper.createObjectNode(); + btn3.put("type", "reply"); + ObjectNode reply3 = objectMapper.createObjectNode(); + reply3.put("id", "MENU_BOOKING"); + reply3.put("title", btn3Title); + btn3.set("reply", reply3); + buttonsArray.add(btn3); + + interactiveNode.set("action", actionNode); + requestBody.set("interactive", interactiveNode); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(accessToken); + + HttpEntity entity = new HttpEntity<>(requestBody.toString(), headers); + logger.info("Sending Main Menu interactive message to {}", recipientPhoneNumber); + restTemplate.postForEntity(url, entity, String.class); + } catch (org.springframework.web.client.HttpStatusCodeException e) { + logger.error("Error sending main menu interactive message. Status: {}, Response: {}", e.getStatusCode(), e.getResponseBodyAsString(), e); + } catch (Exception e) { + logger.error("Error sending main menu interactive message", e); + } + } + + private void sendMessage(String phoneNumberId, String recipientPhoneNumber, String text) { + String url = "https://graph.facebook.com/v20.0/" + phoneNumberId + "/messages"; + + try { + ObjectNode requestBody = objectMapper.createObjectNode(); + requestBody.put("messaging_product", "whatsapp"); + requestBody.put("recipient_type", "individual"); + requestBody.put("to", recipientPhoneNumber); + requestBody.put("type", "text"); + + ObjectNode textNode = objectMapper.createObjectNode(); + textNode.put("preview_url", false); + textNode.put("body", text); + requestBody.set("text", textNode); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(accessToken); + + HttpEntity entity = new HttpEntity<>(requestBody.toString(), headers); + + logger.info("Sending reply to {}: {}", recipientPhoneNumber, text); + ResponseEntity response = restTemplate.postForEntity(url, entity, String.class); + logger.info("Response from WhatsApp API: Status={}", response.getStatusCode()); + } catch (org.springframework.web.client.HttpStatusCodeException e) { + logger.error("Failed to send WhatsApp message to {}. Status: {}, Response: {}", recipientPhoneNumber, e.getStatusCode(), e.getResponseBodyAsString(), e); + } catch (Exception e) { + logger.error("Failed to send WhatsApp message to " + recipientPhoneNumber, e); + } + } + + private void sendInteractiveButtons(String bodyText, String[] ids, String[] titles) { + String phoneNumberId = currentPhoneId.get(); + String recipientPhoneNumber = currentFrom.get(); + + UserState uState = userStateRepository.findById(recipientPhoneNumber).orElse(null); + if (uState != null && !"MAIN_MENU".equals(uState.getState()) && !"LANGUAGE_SELECTION".equals(uState.getState())) { + String restartText; + if ("en".equals(uState.getLanguagePreference())) { + restartText = "\n\n(Press 0 to restart the conversation)"; + } else if ("hi".equals(uState.getLanguagePreference())) { + restartText = "\n\n(बातचीत को फिर से शुरू करने के लिए 0 दबाएं)"; + } else { + restartText = "\n\n(உரையாடலை மீண்டும் தொடங்க 0 ஐ அழுத்தவும்)"; + } + bodyText += restartText; + } + if (phoneNumberId == null || recipientPhoneNumber == null) return; + String url = "https://graph.facebook.com/v20.0/" + phoneNumberId + "/messages"; + try { + ObjectNode requestBody = objectMapper.createObjectNode(); + requestBody.put("messaging_product", "whatsapp"); + requestBody.put("recipient_type", "individual"); + requestBody.put("to", recipientPhoneNumber); + requestBody.put("type", "interactive"); + + ObjectNode interactiveNode = objectMapper.createObjectNode(); + interactiveNode.put("type", "button"); + + ObjectNode bodyNode = objectMapper.createObjectNode(); + bodyNode.put("text", bodyText); + interactiveNode.set("body", bodyNode); + + ObjectNode actionNode = objectMapper.createObjectNode(); + com.fasterxml.jackson.databind.node.ArrayNode buttonsArray = actionNode.putArray("buttons"); + + for (int i = 0; i < ids.length; i++) { + ObjectNode btn = objectMapper.createObjectNode(); + btn.put("type", "reply"); + ObjectNode reply = objectMapper.createObjectNode(); + reply.put("id", ids[i]); + reply.put("title", titles[i]); + btn.set("reply", reply); + buttonsArray.add(btn); + } + + interactiveNode.set("action", actionNode); + requestBody.set("interactive", interactiveNode); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(accessToken); + + HttpEntity entity = new HttpEntity<>(requestBody.toString(), headers); + restTemplate.postForEntity(url, entity, String.class); + } catch (org.springframework.web.client.HttpStatusCodeException e) { + logger.error("Error sending interactive buttons. Status: {}, Response: {}", e.getStatusCode(), e.getResponseBodyAsString(), e); + } catch (Exception e) { + logger.error("Error sending interactive buttons", e); + } + } + + private void sendInteractiveList(String bodyText, String buttonText, String sectionTitle, String[] ids, String[] titles) { + String phoneNumberId = currentPhoneId.get(); + String recipientPhoneNumber = currentFrom.get(); + if (phoneNumberId == null || recipientPhoneNumber == null) return; + String url = "https://graph.facebook.com/v20.0/" + phoneNumberId + "/messages"; + + try { + ObjectNode requestBody = objectMapper.createObjectNode(); + requestBody.put("messaging_product", "whatsapp"); + requestBody.put("recipient_type", "individual"); + requestBody.put("to", recipientPhoneNumber); + requestBody.put("type", "interactive"); + + ObjectNode interactiveNode = objectMapper.createObjectNode(); + interactiveNode.put("type", "list"); + + ObjectNode bodyNode = objectMapper.createObjectNode(); + + UserState uState = userStateRepository.findById(recipientPhoneNumber).orElse(null); + if (uState != null && !"MAIN_MENU".equals(uState.getState()) && !"LANGUAGE_SELECTION".equals(uState.getState())) { + String restartText; + if ("en".equals(uState.getLanguagePreference())) { + restartText = "\n\n(Press 0 to restart the conversation)"; + } else if ("hi".equals(uState.getLanguagePreference())) { + restartText = "\n\n(बातचीत को फिर से शुरू करने के लिए 0 दबाएं)"; + } else { + restartText = "\n\n(உரையாடலை மீண்டும் தொடங்க 0 ஐ அழுத்தவும்)"; + } + bodyText += restartText; + } + bodyNode.put("text", bodyText); + interactiveNode.set("body", bodyNode); + + ObjectNode actionNode = objectMapper.createObjectNode(); + actionNode.put("button", buttonText); + + com.fasterxml.jackson.databind.node.ArrayNode sectionsArray = actionNode.putArray("sections"); + ObjectNode section = objectMapper.createObjectNode(); + section.put("title", sectionTitle); + com.fasterxml.jackson.databind.node.ArrayNode rowsArray = section.putArray("rows"); + + int limit = Math.min(ids.length, 10); + for (int i = 0; i < limit; i++) { + ObjectNode row = objectMapper.createObjectNode(); + row.put("id", ids[i]); + row.put("title", titles[i]); + rowsArray.add(row); + } + sectionsArray.add(section); + + interactiveNode.set("action", actionNode); + requestBody.set("interactive", interactiveNode); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON); + headers.setBearerAuth(accessToken); + + org.springframework.http.HttpEntity entity = new org.springframework.http.HttpEntity<>(requestBody.toString(), headers); + restTemplate.postForEntity(url, entity, String.class); + } catch (org.springframework.web.client.HttpStatusCodeException e) { + logger.error("Error sending interactive list. Status: {}, Response: {}", e.getStatusCode(), e.getResponseBodyAsString(), e); + } catch (Exception e) { + logger.error("Error sending interactive list", e); + } + } + + private void sendPaginatedButtons(UserState state, String bodyText, String[] ids, String[] titles) { + if (ids.length <= 3) { + sendInteractiveButtons(bodyText, ids, titles); + } else { + String buttonText; + String sectionTitle; + if ("en".equals(state.getLanguagePreference())) { + buttonText = "Select Option"; + sectionTitle = "Options"; + } else if ("hi".equals(state.getLanguagePreference())) { + buttonText = "विकल्प चुनें"; + sectionTitle = "विकल्प"; + } else { + buttonText = "தேர்ந்தெடு"; + sectionTitle = "விருப்பங்கள்"; + } + sendInteractiveList(bodyText, buttonText, sectionTitle, ids, titles); + } + } + + // ========================================== + // NEW WORKFLOW IMPLEMENTATION + // ========================================== + private String handleNewWorkflowState(UserState state, String input) { + String trimmedInput = input.trim(); + String upperInput = trimmedInput.toUpperCase(); + + // Navigation Commands + if (upperInput.equals("NEW_FLOW_MAIN_MENU") || trimmedInput.equalsIgnoreCase("Main Menu") || trimmedInput.equals("முதன்மை மெனு") || trimmedInput.equals("मुख्य सूची")) { + state.setState("MAIN_MENU"); + return showNewMainMenu(state); + } + + if (upperInput.equals("NEW_FLOW_EXIT") || trimmedInput.equalsIgnoreCase("Exit") || trimmedInput.equals("வெளியேறு") || trimmedInput.equals("बाहर निकलें")) { + state.setState("EXPIRED"); + return getMessage("new.exit.message", state); + } + + if (upperInput.equals("NEW_FLOW_BACK") || trimmedInput.equalsIgnoreCase("Back") || trimmedInput.equals("பின்னால்") || trimmedInput.equals("वापस") || trimmedInput.contains("Back") || trimmedInput.contains("வாபஸ்") || trimmedInput.contains("वापस")) { + goBackInNewWorkflow(state); + trimmedInput = ""; + } + + if ("LANGUAGE_SELECTION".equals(state.getState())) { + return handleNewLanguageSelection(state, trimmedInput); + } + + switch (state.getState()) { + case "MAIN_MENU": + return handleNewMainMenu(state, trimmedInput); + case "NEW_EMERGENCY": + return handleNewEmergency(state, trimmedInput); + case "NEW_ASSISTANCE": + return handleNewAssistance(state, trimmedInput); + case "NEW_JOB_PHONE": + return handleNewJobPhone(state, trimmedInput); + case "NEW_JOB_EMAIL": + return handleNewJobEmail(state, trimmedInput); + case "NEW_JOB_CONFIRM": + return handleNewJobConfirm(state, trimmedInput); + case "NEW_DOC_SPECIALITY": + return handleNewDocSpeciality(state, trimmedInput); + case "NEW_DOC_DOCTOR": + return handleNewDocDoctor(state, trimmedInput); + case "NEW_DOC_DATE": + return handleNewDocDate(state, trimmedInput); + case "NEW_DOC_TIME": + return handleNewDocTime(state, trimmedInput); + case "NEW_DOC_CONFIRM": + return handleNewDocConfirm(state, trimmedInput); + case "NEW_PKG_SELECT": + return handleNewPkgSelect(state, trimmedInput); + case "NEW_PKG_COUPON": + return handleNewPkgCoupon(state, trimmedInput); + case "NEW_PKG_DATE": + return handleNewPkgDate(state, trimmedInput); + case "NEW_PKG_TIME": + return handleNewPkgTime(state, trimmedInput); + case "NEW_PKG_CONFIRM": + return handleNewPkgConfirm(state, trimmedInput); + case "NEW_INV_TYPE": + return handleNewInvType(state, trimmedInput); + case "NEW_BLOOD_SELECT": + return handleNewBloodSelect(state, trimmedInput); + case "NEW_BLOOD_CONFIRM_TEST": + return handleNewBloodConfirmTest(state, trimmedInput); + case "NEW_BLOOD_DATE": + return handleNewBloodDate(state, trimmedInput); + case "NEW_BLOOD_TIME": + return handleNewBloodTime(state, trimmedInput); + case "NEW_BLOOD_CONFIRM": + return handleNewBloodConfirm(state, trimmedInput); + case "NEW_RADIO_SELECT": + return handleNewRadioSelect(state, trimmedInput); + case "NEW_RADIO_CONFIRM_TEST": + return handleNewRadioConfirmTest(state, trimmedInput); + case "NEW_RADIO_DATE": + return handleNewRadioDate(state, trimmedInput); + case "NEW_RADIO_TIME": + return handleNewRadioTime(state, trimmedInput); + case "NEW_RADIO_CONFIRM": + return handleNewRadioConfirm(state, trimmedInput); + default: + state.setState("MAIN_MENU"); + return showNewMainMenu(state); + } + } + + private String handleNewLanguageSelection(UserState state, String input) { + if ("LANG_EN".equals(input) || "1".equals(input) || input.equalsIgnoreCase("English")) { + state.setLanguagePreference("en"); + state.setState("MAIN_MENU"); + return showNewMainMenu(state); + } else if ("LANG_TA".equals(input) || "2".equals(input) || input.equals("தமிழ்")) { + state.setLanguagePreference("ta"); + state.setState("MAIN_MENU"); + return showNewMainMenu(state); + } else if ("LANG_HI".equals(input) || "3".equals(input) || input.equals("हिंदी") || input.equalsIgnoreCase("Hindi")) { + state.setLanguagePreference("hi"); + state.setState("MAIN_MENU"); + return showNewMainMenu(state); + } + return "INTERACTIVE:LANG"; + } + + private void goBackInNewWorkflow(UserState state) { + String curr = state.getState(); + if (curr == null) { + state.setState("MAIN_MENU"); + return; + } + switch (curr) { + case "NEW_EMERGENCY": + case "NEW_ASSISTANCE": + case "NEW_JOB_PHONE": + case "NEW_DOC_SPECIALITY": + case "NEW_PKG_SELECT": + case "NEW_INV_TYPE": + state.setState("MAIN_MENU"); + break; + case "NEW_JOB_EMAIL": + state.setState("NEW_JOB_PHONE"); + break; + case "NEW_JOB_CONFIRM": + state.setState("NEW_JOB_EMAIL"); + break; + case "NEW_DOC_DOCTOR": + state.setState("NEW_DOC_SPECIALITY"); + break; + case "NEW_DOC_DATE": + state.setState("NEW_DOC_DOCTOR"); + break; + case "NEW_DOC_TIME": + state.setState("NEW_DOC_DATE"); + break; + case "NEW_DOC_CONFIRM": + state.setState("NEW_DOC_TIME"); + break; + case "NEW_PKG_COUPON": + state.setState("NEW_PKG_SELECT"); + break; + case "NEW_PKG_DATE": + state.setState("NEW_PKG_COUPON"); + break; + case "NEW_PKG_TIME": + state.setState("NEW_PKG_DATE"); + break; + case "NEW_PKG_CONFIRM": + state.setState("NEW_PKG_TIME"); + break; + case "NEW_BLOOD_SELECT": + case "NEW_RADIO_SELECT": + state.setState("NEW_INV_TYPE"); + break; + case "NEW_BLOOD_CONFIRM_TEST": + state.setState("NEW_BLOOD_SELECT"); + break; + case "NEW_BLOOD_DATE": + state.setState("NEW_BLOOD_CONFIRM_TEST"); + break; + case "NEW_BLOOD_TIME": + state.setState("NEW_BLOOD_DATE"); + break; + case "NEW_BLOOD_CONFIRM": + state.setState("NEW_BLOOD_TIME"); + break; + case "NEW_RADIO_CONFIRM_TEST": + state.setState("NEW_RADIO_SELECT"); + break; + case "NEW_RADIO_DATE": + state.setState("NEW_RADIO_CONFIRM_TEST"); + break; + case "NEW_RADIO_TIME": + state.setState("NEW_RADIO_DATE"); + break; + case "NEW_RADIO_CONFIRM": + state.setState("NEW_RADIO_TIME"); + break; + default: + state.setState("MAIN_MENU"); + break; + } + } + + private String showNewMainMenu(UserState state) { + String body = getMessage("new.menu.title", state); + String[] ids = new String[]{"NEW_MENU_EMERGENCY", "NEW_MENU_DOCTOR", "NEW_MENU_PACKAGES", "NEW_MENU_INVESTIGATION", "NEW_MENU_JOB", "NEW_MENU_ASSISTANCE"}; + String[] titles = new String[]{ + getMessage("new.menu.opt1", state), + getMessage("new.menu.opt2", state), + getMessage("new.menu.opt3", state), + getMessage("new.menu.opt4", state), + getMessage("new.menu.opt5", state), + getMessage("new.menu.opt6", state) + }; + sendPaginatedButtons(state, body, ids, titles); + return null; + } + + private String handleNewMainMenu(UserState state, String input) { + switch (input) { + case "1": case "NEW_MENU_EMERGENCY": + state.setState("NEW_EMERGENCY"); + return handleNewEmergency(state, ""); + case "2": case "NEW_MENU_DOCTOR": + state.setState("NEW_DOC_SPECIALITY"); + return handleNewDocSpeciality(state, ""); + case "3": case "NEW_MENU_PACKAGES": + state.setState("NEW_PKG_SELECT"); + return handleNewPkgSelect(state, ""); + case "4": case "NEW_MENU_INVESTIGATION": + state.setState("NEW_INV_TYPE"); + return handleNewInvType(state, ""); + case "5": case "NEW_MENU_JOB": + state.setState("NEW_JOB_PHONE"); + return handleNewJobPhone(state, ""); + case "6": case "NEW_MENU_ASSISTANCE": + state.setState("NEW_ASSISTANCE"); + return handleNewAssistance(state, ""); + default: + return showNewMainMenu(state); + } + } + + private String handleNewEmergency(UserState state, String input) { + if ("NEW_EMERGENCY_MAIN_MENU".equals(input) || "1".equals(input)) { + state.setState("MAIN_MENU"); + return showNewMainMenu(state); + } else if ("NEW_EMERGENCY_EXIT".equals(input) || "2".equals(input)) { + state.setState("EXPIRED"); + return getMessage("new.exit.message", state); + } + + sendInteractiveButtons(getMessage("new.emergency.text", state), + new String[]{"NEW_EMERGENCY_MAIN_MENU", "NEW_EMERGENCY_EXIT"}, + new String[]{getMessage("new.btn.main_menu", state), getMessage("new.btn.exit", state)}); + return null; + } + + private String handleNewAssistance(UserState state, String input) { + if ("NEW_ASSISTANCE_MAIN_MENU".equals(input) || "1".equals(input)) { + state.setState("MAIN_MENU"); + return showNewMainMenu(state); + } else if ("NEW_ASSISTANCE_EXIT".equals(input) || "2".equals(input)) { + state.setState("EXPIRED"); + return getMessage("new.exit.message", state); + } + + sendInteractiveButtons(getMessage("new.assistance.text", state), + new String[]{"NEW_ASSISTANCE_MAIN_MENU", "NEW_ASSISTANCE_EXIT"}, + new String[]{getMessage("new.btn.main_menu", state), getMessage("new.btn.exit", state)}); + return null; + } + + private String handleNewJobPhone(UserState state, String input) { + if (input.isEmpty()) { + return getMessage("new.job.ask_phone", state); + } + String cleanPhone = input.replaceAll("\\D+", ""); + if (cleanPhone.length() != 10) { + return getMessage("new.job.invalid_phone", state) + "\n\n" + getMessage("new.job.ask_phone", state); + } + state.setSubCategory(cleanPhone); + state.setState("NEW_JOB_EMAIL"); + return getMessage("new.job.ask_email", state); + } + + private String handleNewJobEmail(UserState state, String input) { + if (input.isEmpty()) { + return getMessage("new.job.ask_email", state); + } + if (!input.matches("^[A-Za-z0-9+_.-]+@(.+)$")) { + return getMessage("new.job.invalid_email", state) + "\n\n" + getMessage("new.job.ask_email", state); + } + state.setAddress(input); + state.setState("NEW_JOB_CONFIRM"); + return handleNewJobConfirm(state, ""); + } + + private String handleNewJobConfirm(UserState state, String input) { + if ("NEW_JOB_MAIN_MENU".equals(input) || "1".equals(input)) { + state.setState("MAIN_MENU"); + return showNewMainMenu(state); + } else if ("NEW_JOB_EXIT".equals(input) || "2".equals(input)) { + state.setState("EXPIRED"); + return getMessage("new.exit.message", state); + } + + sendInteractiveButtons(getMessage("new.job.success", state), + new String[]{"NEW_JOB_MAIN_MENU", "NEW_JOB_EXIT"}, + new String[]{getMessage("new.btn.main_menu", state), getMessage("new.btn.exit", state)}); + return null; + } + + private String handleNewDocSpeciality(UserState state, String input) { + if (input.isEmpty()) { + sendPaginatedButtons(state, getMessage("new.doc.dept.title", state), + new String[]{"DEPT_GEN_MED", "DEPT_CARDIO", "DEPT_ORTHO", "DEPT_NEURO", "DEPT_PEDS", "DEPT_GYNO", "DEPT_DERM", "DEPT_ENT", "DEPT_OPHTH"}, + new String[]{getMessage("doc.dept.opt1", state), getMessage("doc.dept.opt2", state), getMessage("doc.dept.opt3", state), getMessage("doc.dept.opt4", state), getMessage("doc.dept.opt5", state), getMessage("doc.dept.opt6", state), getMessage("doc.dept.opt7", state), getMessage("doc.dept.opt8", state), getMessage("doc.dept.opt9", state)}); + return null; + } + + String dept = ""; + switch (input) { + case "1": case "DEPT_GEN_MED": dept = "General Medicine"; break; + case "2": case "DEPT_CARDIO": dept = "Cardiology"; break; + case "3": case "DEPT_ORTHO": dept = "Orthopedics"; break; + case "4": case "DEPT_NEURO": dept = "Neurology"; break; + case "5": case "DEPT_PEDS": dept = "Pediatrics"; break; + case "6": case "DEPT_GYNO": dept = "Gynecology"; break; + case "7": case "DEPT_DERM": dept = "Dermatology"; break; + case "8": case "DEPT_ENT": dept = "ENT"; break; + case "9": case "DEPT_OPHTH": dept = "Ophthalmology"; break; + default: return "Invalid option."; + } + state.setCategory(dept); + state.setState("NEW_DOC_DOCTOR"); + return handleNewDocDoctor(state, ""); + } + + private String handleNewDocDoctor(UserState state, String input) { + if (input.isEmpty()) { + sendPaginatedButtons(state, getMessage("new.doc.sel.title", state), + new String[]{"DOC_RAJESH", "DOC_PRIYA", "DOC_ARAVIND"}, + new String[]{"Dr. Rajesh Kumar", "Dr. Priya Sharma", "Dr. Aravind"}); + return null; + } + + String doc = ""; + switch(input) { + case "1": case "DOC_RAJESH": doc = "Dr. Rajesh Kumar"; break; + case "2": case "DOC_PRIYA": doc = "Dr. Priya Sharma"; break; + case "3": case "DOC_ARAVIND": doc = "Dr. Aravind"; break; + default: return "Invalid option."; + } + state.setSelectedItem(doc); + state.setState("NEW_DOC_DATE"); + return getMessage("new.doc.ask_date", state); + } + + private String handleNewDocDate(UserState state, String input) { + if (input.isEmpty()) { + return getMessage("new.doc.ask_date", state); + } + if (!isValidDate(input)) { + return getMessage("new.doc.invalid_date", state); + } + state.setBookingDate(input); + state.setState("NEW_DOC_TIME"); + return handleNewDocTime(state, ""); + } + + private String handleNewDocTime(UserState state, String input) { + if (input.isEmpty()) { + sendPaginatedButtons(state, getMessage("new.doc.time.title", state), + new String[]{"TIME_9AM", "TIME_10AM", "TIME_11_30AM", "TIME_2PM", "TIME_4PM"}, + new String[]{"09:00 AM", "10:00 AM", "11:30 AM", "02:00 PM", "04:00 PM"}); + return null; + } + + String time = ""; + switch(input) { + case "1": case "TIME_9AM": time = "09:00 AM"; break; + case "2": case "TIME_10AM": time = "10:00 AM"; break; + case "3": case "TIME_11_30AM": time = "11:30 AM"; break; + case "4": case "TIME_2PM": time = "02:00 PM"; break; + case "5": case "TIME_4PM": time = "04:00 PM"; break; + default: time = input; break; + } + state.setBookingTime(time); + state.setPrice("₹600"); + state.setState("NEW_DOC_CONFIRM"); + return handleNewDocConfirm(state, ""); + } + + private String handleNewDocConfirm(UserState state, String input) { + if ("APT_CONFIRM".equals(input) || "1".equals(input) || input.equalsIgnoreCase("Confirm Appointment") || input.equalsIgnoreCase("Confirm")) { + Appointment app = new Appointment(); + app.setPhoneNumber(state.getPhoneNumber()); + String appId = "APT" + (100000 + new Random().nextInt(900000)); + app.setAppointmentId(appId); + app.setDepartment(state.getCategory()); + app.setDoctor(state.getSelectedItem()); + app.setAppointmentDate(state.getBookingDate()); + app.setAppointmentTime(state.getBookingTime()); + app.setFee(state.getPrice()); + app.setPatientId(state.getPatientId()); + app.setStatus("Confirmed"); + appointmentRepository.save(app); + + state.setState("NEW_EMERGENCY"); + String successMsg = getMessage("new.doc.success", state, state.getSelectedItem(), state.getCategory(), state.getBookingDate(), state.getBookingTime(), appId); + sendInteractiveButtons(successMsg, + new String[]{"NEW_EMERGENCY_MAIN_MENU", "NEW_EMERGENCY_EXIT"}, + new String[]{getMessage("new.btn.main_menu", state), getMessage("new.btn.exit", state)}); + return null; + } else if ("APT_CANCEL".equals(input) || "2".equals(input) || input.equalsIgnoreCase("Cancel")) { + state.setState("MAIN_MENU"); + return showNewMainMenu(state); + } + + String summary = getMessage("new.doc.summary", state, state.getSelectedItem(), state.getCategory(), state.getBookingDate(), state.getBookingTime()); + sendInteractiveButtons(summary, + new String[]{"APT_CONFIRM", "APT_CANCEL"}, + new String[]{getMessage("new.doc.confirm_btn", state), getMessage("new.doc.cancel_btn", state)}); + return null; + } + + private String handleNewPkgSelect(UserState state, String input) { + if (input.isEmpty()) { + sendPaginatedButtons(state, getMessage("new.pkg.title", state), + new String[]{"PKG_GEN", "PKG_CARDIO", "PKG_MATERNITY"}, + new String[]{getMessage("new.pkg.opt1", state), getMessage("new.pkg.opt2", state), getMessage("new.pkg.opt3", state)}); + return null; + } + + String pkg = ""; + String price = ""; + switch (input) { + case "1": case "PKG_GEN": pkg = "General Health Checkup"; price = "₹1,500"; break; + case "2": case "PKG_CARDIO": pkg = "Advanced Cardiac Package"; price = "₹3,000"; break; + case "3": case "PKG_MATERNITY": pkg = "Maternity Care Package"; price = "₹2,500"; break; + default: return "Invalid option."; + } + state.setSelectedItem(pkg); + state.setPrice(price); + state.setState("NEW_PKG_COUPON"); + return handleNewPkgCoupon(state, ""); + } + + private String handleNewPkgCoupon(UserState state, String input) { + if (input.isEmpty()) { + sendInteractiveButtons(getMessage("new.pkg.ask_coupon", state), new String[]{"NEW_PKG_SKIP"}, new String[]{getMessage("new.pkg.btn_skip", state)}); + return null; + } + + if ("NEW_PKG_SKIP".equals(input) || "skip".equalsIgnoreCase(input)) { + state.setSubCategory("NONE"); + state.setState("NEW_PKG_DATE"); + return getMessage("new.pkg.ask_date", state); + } + + if ("RMC10".equalsIgnoreCase(input)) { + String origStr = state.getPrice().replaceAll("[^\\d]", ""); + int orig = Integer.parseInt(origStr); + int discounted = (int) (orig * 0.9); + state.setPrice("₹" + String.format("%,d", discounted)); + state.setSubCategory("RMC10 (10% Off)"); + state.setState("NEW_PKG_DATE"); + return getMessage("new.pkg.ask_date", state); + } else if ("SAVE20".equalsIgnoreCase(input)) { + String origStr = state.getPrice().replaceAll("[^\\d]", ""); + int orig = Integer.parseInt(origStr); + int discounted = (int) (orig * 0.8); + state.setPrice("₹" + String.format("%,d", discounted)); + state.setSubCategory("SAVE20 (20% Off)"); + state.setState("NEW_PKG_DATE"); + return getMessage("new.pkg.ask_date", state); + } else { + return getMessage("new.pkg.invalid_coupon", state); + } + } + + private String handleNewPkgDate(UserState state, String input) { + if (input.isEmpty()) { + return getMessage("new.pkg.ask_date", state); + } + if (!isValidDate(input)) { + return getMessage("new.doc.invalid_date", state); + } + state.setBookingDate(input); + state.setState("NEW_PKG_TIME"); + return handleNewPkgTime(state, ""); + } + + private String handleNewPkgTime(UserState state, String input) { + if (input.isEmpty()) { + sendPaginatedButtons(state, getMessage("new.pkg.ask_time", state), + new String[]{"PKG_TIME_9AM", "PKG_TIME_11AM", "PKG_TIME_3PM"}, + new String[]{"09:00 AM", "11:00 AM", "03:00 PM"}); + return null; + } + + String time = ""; + switch (input) { + case "1": case "PKG_TIME_9AM": time = "09:00 AM"; break; + case "2": case "PKG_TIME_11AM": time = "11:00 AM"; break; + case "3": case "PKG_TIME_3PM": time = "03:00 PM"; break; + default: time = input; break; + } + state.setBookingTime(time); + state.setState("NEW_PKG_CONFIRM"); + return handleNewPkgConfirm(state, ""); + } + + private String handleNewPkgConfirm(UserState state, String input) { + if ("PKG_CONFIRM".equals(input) || "1".equals(input) || input.equalsIgnoreCase("Confirm Booking") || input.equalsIgnoreCase("Confirm")) { + ServiceBooking booking = new ServiceBooking(); + booking.setPhoneNumber(state.getPhoneNumber()); + String bookingId = "RMC-HC-" + (10000 + new Random().nextInt(90000)); + booking.setBookingId(bookingId); + booking.setServiceName(state.getSelectedItem()); + booking.setPrice(state.getPrice()); + booking.setAddress("N/A"); + booking.setBookingDate(state.getBookingDate()); + booking.setBookingTime(state.getBookingTime()); + booking.setPatientId(state.getPatientId()); + booking.setStatus("CONFIRMED"); + serviceBookingRepository.save(booking); + + state.setState("NEW_EMERGENCY"); + String successMsg = getMessage("new.pkg.success", state, state.getSelectedItem(), state.getBookingDate(), state.getBookingTime(), state.getPrice(), bookingId); + sendInteractiveButtons(successMsg, + new String[]{"NEW_EMERGENCY_MAIN_MENU", "NEW_EMERGENCY_EXIT"}, + new String[]{getMessage("new.btn.main_menu", state), getMessage("new.btn.exit", state)}); + return null; + } else if ("PKG_CANCEL".equals(input) || "2".equals(input) || input.equalsIgnoreCase("Cancel")) { + state.setState("MAIN_MENU"); + return showNewMainMenu(state); + } + + String summary = getMessage("new.pkg.confirm_title", state) + "\n\n" + getMessage("new.pkg.summary", state, state.getSelectedItem(), state.getBookingDate(), state.getBookingTime(), state.getPrice(), state.getSubCategory()); + sendInteractiveButtons(summary, + new String[]{"PKG_CONFIRM", "PKG_CANCEL"}, + new String[]{getMessage("new.btn.confirm", state), getMessage("new.btn.cancel", state)}); + return null; + } + + private String handleNewInvType(UserState state, String input) { + if (input.isEmpty()) { + sendPaginatedButtons(state, getMessage("new.inv.title", state), + new String[]{"INV_BLOOD", "INV_RADIO"}, + new String[]{getMessage("new.inv.opt1", state), getMessage("new.inv.opt2", state)}); + return null; + } + + if ("1".equals(input) || "INV_BLOOD".equals(input)) { + state.setCategory("Blood / Lab"); + state.setState("NEW_BLOOD_SELECT"); + return handleNewBloodSelect(state, ""); + } else if ("2".equals(input) || "INV_RADIO".equals(input)) { + state.setCategory("Radiology"); + state.setState("NEW_RADIO_SELECT"); + return handleNewRadioSelect(state, ""); + } + return "Invalid option."; + } + + private String handleNewBloodSelect(UserState state, String input) { + if (input.isEmpty()) { + sendPaginatedButtons(state, getMessage("new.blood.title", state), + new String[]{"BLOOD_CBC", "BLOOD_LFT", "BLOOD_LIPID", "BLOOD_HBA1C"}, + new String[]{getMessage("new.blood.opt1", state), getMessage("new.blood.opt2", state), getMessage("new.blood.opt3", state), getMessage("new.blood.opt4", state)}); + return null; + } + + String test = ""; + String price = ""; + switch (input) { + case "1": case "BLOOD_CBC": test = "Complete Blood Count (CBC)"; price = "₹300"; break; + case "2": case "BLOOD_LFT": test = "Liver Function Test (LFT)"; price = "₹500"; break; + case "3": case "BLOOD_LIPID": test = "Lipid Profile"; price = "₹450"; break; + case "4": case "BLOOD_HBA1C": test = "HbA1c"; price = "₹350"; break; + default: return "Invalid option."; + } + state.setSelectedItem(test); + state.setPrice(price); + state.setState("NEW_BLOOD_CONFIRM_TEST"); + return handleNewBloodConfirmTest(state, ""); + } + + private String handleNewBloodConfirmTest(UserState state, String input) { + if ("BLOOD_CONFIRM_YES".equals(input) || "1".equals(input) || input.equalsIgnoreCase("Confirm") || input.equalsIgnoreCase("Yes")) { + state.setState("NEW_BLOOD_DATE"); + return getMessage("booking.test.ask_date", state); + } else if ("BLOOD_CONFIRM_NO".equals(input) || "2".equals(input) || input.equalsIgnoreCase("Cancel") || input.equalsIgnoreCase("No")) { + state.setState("NEW_BLOOD_SELECT"); + return handleNewBloodSelect(state, ""); + } + + String confirmMsg = getMessage("new.blood.confirm", state, state.getSelectedItem(), state.getPrice()); + sendInteractiveButtons(confirmMsg, + new String[]{"BLOOD_CONFIRM_YES", "BLOOD_CONFIRM_NO"}, + new String[]{getMessage("new.blood.btn_confirm", state), getMessage("new.blood.btn_cancel", state)}); + return null; + } + + private String handleNewBloodDate(UserState state, String input) { + if (input.isEmpty()) { + return getMessage("booking.test.ask_date", state); + } + if (!isValidDate(input)) { + return getMessage("new.doc.invalid_date", state); + } + state.setBookingDate(input); + state.setState("NEW_BLOOD_TIME"); + return handleNewBloodTime(state, ""); + } + + private String handleNewBloodTime(UserState state, String input) { + if (input.isEmpty()) { + sendPaginatedButtons(state, getMessage("booking.test.ask_time.title", state), + new String[]{"SLOT_7AM", "SLOT_9AM", "SLOT_11AM"}, + new String[]{getMessage("booking.test.opt1", state), getMessage("booking.test.opt2", state), getMessage("booking.test.opt3", state)}); + return null; + } + + String time = ""; + switch (input) { + case "1": case "SLOT_7AM": time = "7 AM - 9 AM"; break; + case "2": case "SLOT_9AM": time = "9 AM - 11 AM"; break; + case "3": case "SLOT_11AM": time = "11 AM - 1 PM"; break; + default: time = input; break; + } + state.setBookingTime(time); + state.setState("NEW_BLOOD_CONFIRM"); + return handleNewBloodConfirm(state, ""); + } + + private String handleNewBloodConfirm(UserState state, String input) { + if ("BLOOD_FINAL_CONFIRM".equals(input) || "1".equals(input) || input.equalsIgnoreCase("Confirm Booking") || input.equalsIgnoreCase("Confirm")) { + TestBooking booking = new TestBooking(); + booking.setPhoneNumber(state.getPhoneNumber()); + String bookingId = "LAB" + (1000000 + new Random().nextInt(9000000)); + booking.setBookingId(bookingId); + booking.setPatientId(state.getPatientId()); + booking.setPackageName(state.getSelectedItem()); + booking.setPrice(state.getPrice()); + booking.setCollectionType("Visit Laboratory"); + booking.setAddress("Rajalakshmi Medical College Laboratory"); + booking.setBookingDate(state.getBookingDate()); + booking.setBookingTime(state.getBookingTime()); + booking.setStatus("CONFIRMED"); + testBookingRepository.save(booking); + + state.setState("NEW_EMERGENCY"); + String successMsg = getMessage("new.booking.success", state, state.getSelectedItem(), state.getBookingDate(), state.getBookingTime(), state.getPrice(), bookingId); + sendInteractiveButtons(successMsg, + new String[]{"NEW_EMERGENCY_MAIN_MENU", "NEW_EMERGENCY_EXIT"}, + new String[]{getMessage("new.btn.main_menu", state), getMessage("new.btn.exit", state)}); + return null; + } else if ("BLOOD_FINAL_CANCEL".equals(input) || "2".equals(input) || input.equalsIgnoreCase("Cancel")) { + state.setState("MAIN_MENU"); + return showNewMainMenu(state); + } + + String summary = getMessage("new.pkg.confirm_title", state) + "\n\n" + getMessage("new.pkg.summary", state, state.getSelectedItem(), state.getBookingDate(), state.getBookingTime(), state.getPrice(), "NONE"); + sendInteractiveButtons(summary, + new String[]{"BLOOD_FINAL_CONFIRM", "BLOOD_FINAL_CANCEL"}, + new String[]{getMessage("new.btn.confirm", state), getMessage("new.btn.cancel", state)}); + return null; + } + + private String handleNewRadioSelect(UserState state, String input) { + if (input.isEmpty()) { + sendPaginatedButtons(state, getMessage("new.radio.title", state), + new String[]{"RADIO_XRAY", "RADIO_CT", "RADIO_MRI", "RADIO_ULTRASOUND"}, + new String[]{getMessage("new.radio.opt1", state), getMessage("new.radio.opt2", state), getMessage("new.radio.opt3", state), getMessage("new.radio.opt4", state)}); + return null; + } + + String test = ""; + String price = ""; + switch (input) { + case "1": case "RADIO_XRAY": test = "X-Ray"; price = "₹500"; break; + case "2": case "RADIO_CT": test = "CT Scan"; price = "₹2,500"; break; + case "3": case "RADIO_MRI": test = "MRI"; price = "₹5,000"; break; + case "4": case "RADIO_ULTRASOUND": test = "Ultrasound"; price = "₹1,200"; break; + default: return "Invalid option."; + } + state.setSelectedItem(test); + state.setPrice(price); + state.setState("NEW_RADIO_CONFIRM_TEST"); + return handleNewRadioConfirmTest(state, ""); + } + + private String handleNewRadioConfirmTest(UserState state, String input) { + if ("RADIO_CONFIRM_YES".equals(input) || "1".equals(input) || input.equalsIgnoreCase("Confirm") || input.equalsIgnoreCase("Yes")) { + state.setState("NEW_RADIO_DATE"); + return getMessage("booking.test.ask_date", state); + } else if ("RADIO_CONFIRM_NO".equals(input) || "2".equals(input) || input.equalsIgnoreCase("Cancel") || input.equalsIgnoreCase("No")) { + state.setState("NEW_RADIO_SELECT"); + return handleNewRadioSelect(state, ""); + } + + String confirmMsg = getMessage("new.blood.confirm", state, state.getSelectedItem(), state.getPrice()); + sendInteractiveButtons(confirmMsg, + new String[]{"RADIO_CONFIRM_YES", "RADIO_CONFIRM_NO"}, + new String[]{getMessage("new.blood.btn_confirm", state), getMessage("new.blood.btn_cancel", state)}); + return null; + } + + private String handleNewRadioDate(UserState state, String input) { + if (input.isEmpty()) { + return getMessage("booking.test.ask_date", state); + } + if (!isValidDate(input)) { + return getMessage("new.doc.invalid_date", state); + } + state.setBookingDate(input); + state.setState("NEW_RADIO_TIME"); + return handleNewRadioTime(state, ""); + } + + private String handleNewRadioTime(UserState state, String input) { + if (input.isEmpty()) { + sendPaginatedButtons(state, getMessage("booking.test.ask_time.title", state), + new String[]{"SLOT_7AM", "SLOT_9AM", "SLOT_11AM"}, + new String[]{getMessage("booking.test.opt1", state), getMessage("booking.test.opt2", state), getMessage("booking.test.opt3", state)}); + return null; + } + + String time = ""; + switch (input) { + case "1": case "SLOT_7AM": time = "7 AM - 9 AM"; break; + case "2": case "SLOT_9AM": time = "9 AM - 11 AM"; break; + case "3": case "SLOT_11AM": time = "11 AM - 1 PM"; break; + default: time = input; break; + } + state.setBookingTime(time); + state.setState("NEW_RADIO_CONFIRM"); + return handleNewRadioConfirm(state, ""); + } + + private String handleNewRadioConfirm(UserState state, String input) { + if ("RADIO_FINAL_CONFIRM".equals(input) || "1".equals(input) || input.equalsIgnoreCase("Confirm Booking") || input.equalsIgnoreCase("Confirm")) { + TestBooking booking = new TestBooking(); + booking.setPhoneNumber(state.getPhoneNumber()); + String bookingId = "LAB" + (1000000 + new Random().nextInt(9000000)); + booking.setBookingId(bookingId); + booking.setPatientId(state.getPatientId()); + booking.setPackageName(state.getSelectedItem()); + booking.setPrice(state.getPrice()); + booking.setCollectionType("Visit Laboratory"); + booking.setAddress("Rajalakshmi Medical College Laboratory"); + booking.setBookingDate(state.getBookingDate()); + booking.setBookingTime(state.getBookingTime()); + booking.setStatus("CONFIRMED"); + testBookingRepository.save(booking); + + state.setState("NEW_EMERGENCY"); + String successMsg = getMessage("new.booking.success", state, state.getSelectedItem(), state.getBookingDate(), state.getBookingTime(), state.getPrice(), bookingId); + sendInteractiveButtons(successMsg, + new String[]{"NEW_EMERGENCY_MAIN_MENU", "NEW_EMERGENCY_EXIT"}, + new String[]{getMessage("new.btn.main_menu", state), getMessage("new.btn.exit", state)}); + return null; + } else if ("RADIO_FINAL_CANCEL".equals(input) || "2".equals(input) || input.equalsIgnoreCase("Cancel")) { + state.setState("MAIN_MENU"); + return showNewMainMenu(state); + } + + String summary = getMessage("new.pkg.confirm_title", state) + "\n\n" + getMessage("new.pkg.summary", state, state.getSelectedItem(), state.getBookingDate(), state.getBookingTime(), state.getPrice(), "NONE"); + sendInteractiveButtons(summary, + new String[]{"RADIO_FINAL_CONFIRM", "RADIO_FINAL_CANCEL"}, + new String[]{getMessage("new.btn.confirm", state), getMessage("new.btn.cancel", state)}); + return null; + } + + private boolean isValidDate(String dateStr) { + if (dateStr == null || !dateStr.matches("^\\d{2}/\\d{2}/\\d{4}$")) { + return false; + } + java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("dd/MM/yyyy"); + sdf.setLenient(false); + try { + sdf.parse(dateStr); + return true; + } catch (java.text.ParseException e) { + return false; + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..75dbff5 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,19 @@ +spring.application.name=whatsapp-automation +server.port=8085 + +whatsapp.access-token=EAAOZCONMSXCkBSQhQ1hh9oJ7J9kYvZBvZAQjz3SH0KKbZC709ES9afccFJ95uCh8TuWVgxU6cFjZALkTSfGBy15FU1uwdWDimSMUytSmW7NzSLQ9RCq6RgONcIbKuNRioYwO2uPJu8RqhmoLeyztqzzBpGupN4jn9yOkvCXClL0IvfFdFwyvTb4zl3uR0ZB95bQ6bpFDvH0ZAM8fwYirdM6hgIjhoUitKkGRU6ONbADCD5HvZAUSZB5idETz0C3WR8LYMZBZAbZCrwZA2Iw8L2BcDTIEI +whatsapp.verify-token=my_whatsapp_verify_token_123 + +spring.datasource.url=jdbc:postgresql://localhost:5432/rmc_bot +spring.datasource.username=postgres +spring.datasource.password=arjun@132006 +spring.jpa.hibernate.ddl-auto=update +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.show-sql=true + + + + +conversation.inactivity.timeout=1200 +chatbot.new-workflow.enabled=true +spring.messages.encoding=UTF-8 \ No newline at end of file diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties new file mode 100644 index 0000000..e60b562 --- /dev/null +++ b/src/main/resources/messages.properties @@ -0,0 +1,92 @@ +welcome.body=👋 Welcome to Rajalakshmi Medical College! I'm your virtual healthcare assistant. Please choose one of the following options: +menu.btn.services=My Services +menu.btn.tests=My Tests +menu.btn.booking=Book Appointment +menu.services=🩺 My Services\n\nPlease choose a category:\n1️⃣ Nursing Services\n2️⃣ Attender Services\n3️⃣ Physiotherapy\n4️⃣ Medical Procedures\n5️⃣ Digital Consultation\n6️⃣ Home Care Packages\n\nReply with the option number. +menu.tests=🏥 Rajalakshmi Medical College\nWelcome to Laboratory Services.\n\nPlease choose a Health Package Category.\n1️⃣ Fever Panel Tests\n2️⃣ Heart Health\n3️⃣ Joint & Bone\n4️⃣ Master Health Check Plans\n5️⃣ Pregnancy\n6️⃣ Sugar Control Series\n7️⃣ Women's Wellness Plans\n8️⃣ Search Test\n\nReply with the option number. +menu.doctor=👨‍⚕️ Doctor Appointment\nPlease choose an option\n1️⃣ Book New Appointment\n2️⃣ View My Appointments\n3️⃣ Reschedule Appointment\n4️⃣ Cancel Appointment\n\nReply with the option number. +invalid.option=Invalid option. Please try again. + +new.menu.title=How may I help you today? +new.menu.opt1=🚨 Emergency Contact +new.menu.opt2=👨‍⚕️ Doctor Appointment +new.menu.opt3=🏥 Health Packages +new.menu.opt4=🔬 Book Lab Test +new.menu.opt5=💼 Job Enquiry +new.menu.opt6=📞 Assistance +new.menu.btn=Select Option +new.menu.list=Main Menu + +new.emergency.text=For emergency assistance, please contact our emergency department immediately.\n📞 Emergency Contact: +91 44 2715 2000 +new.btn.main_menu=Main Menu +new.btn.exit=Exit +new.btn.back=⬅️ Back +new.exit.message=Thank you for contacting Rajalakshmi Medical College. Have a healthy day! 👋 + +new.assistance.text=Certainly. For assistance, you can contact our reception team.\n📞 Reception Contact Number: +91 44 2715 2000 + +new.job.ask_phone=Please reply with your 10-digit Contact Number. +new.job.invalid_phone=Invalid phone number. Please enter a 10-digit number. +new.job.ask_email=Thank you. Now please reply with your Email ID. +new.job.invalid_email=Invalid email format. Please enter a valid Email ID. +new.job.success=Thank you. Your details have been received successfully.\nOur recruitment team will contact you regarding suitable opportunities.\n📞 HR Contact: +91 44 2715 2000 (hr@rmchospital.com) + +new.doc.dept.title=Please select a speciality. +new.doc.dept.btn=View Specialities +new.doc.dept.list=Specialities +new.doc.sel.title=Please select a Doctor. +new.doc.sel.btn=View Doctors +new.doc.sel.list=Doctors +new.doc.ask_date=Please enter your preferred visit date in DD/MM/YYYY format. +new.doc.invalid_date=Invalid date format or date is invalid. Please reply with a valid date in DD/MM/YYYY format. +new.doc.time.title=Please choose an available time slot. +new.doc.time.btn=View Time Slots +new.doc.time.list=Time Slots +new.doc.summary=👨‍⚕️ Doctor: {0}\n🏥 Speciality: {1}\n📅 Date: {2}\n⏰ Time: {3} +new.doc.confirm_btn=Confirm Appointment +new.doc.cancel_btn=Cancel +new.doc.success=✅ Your appointment has been successfully booked.\n\nDoctor: {0}\nSpeciality: {1}\nDate: {2}\nTime: {3}\nAppointment ID: {4} + +new.pkg.title=Please select a Healthcare Package. +new.pkg.btn=View Packages +new.pkg.list=Packages +new.pkg.opt1=Gen Health (₹1500) +new.pkg.opt2=Cardiac Pkg (₹3000) +new.pkg.opt3=Maternity (₹2500) +new.pkg.ask_coupon=Do you have a coupon code? Please type the code below, or click 'Skip Coupon' to proceed. +new.pkg.btn_skip=Skip Coupon +new.pkg.invalid_coupon=Invalid coupon code. Please try again or click 'Skip Coupon'. +new.pkg.ask_date=Please enter your preferred date in DD/MM/YYYY format. +new.pkg.ask_time=Please select a preferred time slot. +new.pkg.confirm_title=Please confirm your booking details: +new.pkg.summary=📦 Package: {0}\n📅 Date: {1}\n⏰ Time: {2}\n💵 Rate: {3} (Coupon: {4}) +new.pkg.success=✅ Healthcare package scheduled successfully.\n\nPackage: {0}\nDate: {1}\nTime: {2}\nFinal Rate: {3}\nBooking ID: {4} + +new.inv.title=Please choose the investigation type. +new.inv.btn=Choose Type +new.inv.list=Types +new.inv.opt1=🩸 Blood / Lab +new.inv.opt2=🩻 Radiology + +new.blood.title=Please select a blood test. +new.blood.btn=View Tests +new.blood.list=Tests +new.blood.opt1=CBC Test (₹300) +new.blood.opt2=LFT Test (₹500) +new.blood.opt3=Lipid Profile (₹450) +new.blood.opt4=HbA1c Test (₹350) +new.blood.confirm=You have selected:\n🧪 {0}\nRate: {1}\nWould you like to confirm? +new.blood.btn_confirm=Confirm +new.blood.btn_cancel=Cancel + +new.radio.title=Please select a Radiology service. +new.radio.btn=View Services +new.radio.list=Services +new.radio.opt1=X-Ray (₹500) +new.radio.opt2=CT Scan (₹2500) +new.radio.opt3=MRI (₹5000) +new.radio.opt4=Ultrasound (₹1200) + +new.btn.confirm=Confirm Booking +new.btn.cancel=Cancel +new.booking.success=✅ Booking scheduled successfully.\n\nService: {0}\nDate: {1}\nTime: {2}\nRate: {3}\nBooking ID: {4} diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties new file mode 100644 index 0000000..55c7abd --- /dev/null +++ b/src/main/resources/messages_en.properties @@ -0,0 +1,293 @@ +welcome.body=👋 Welcome to Rajalakshmi Medical College! I'm your virtual healthcare assistant. Please choose one of the following options: +menu.btn.services=My Services +menu.btn.tests=My Tests +menu.btn.booking=Book Appointment +menu.services=🩺 My Services\n\nPlease choose a category:\n1️⃣ Nursing Services\n2️⃣ Attender Services\n3️⃣ Physiotherapy\n4️⃣ Medical Procedures\n5️⃣ Digital Consultation\n6️⃣ Home Care Packages\n\nReply with the option number. +menu.tests=🏥 Rajalakshmi Medical College\nWelcome to Laboratory Services.\n\nPlease choose a Health Package Category.\n1️⃣ Fever Panel Tests\n2️⃣ Heart Health\n3️⃣ Joint & Bone\n4️⃣ Master Health Check Plans\n5️⃣ Pregnancy\n6️⃣ Sugar Control Series\n7️⃣ Women's Wellness Plans\n8️⃣ Search Test\n\nReply with the option number. +menu.doctor=👨‍⚕️ Doctor Appointment\nPlease choose an option\n1️⃣ Book New Appointment\n2️⃣ View My Appointments\n3️⃣ Reschedule Appointment\n4️⃣ Cancel Appointment\n\nReply with the option number. +invalid.option=Invalid option. Please try again. + +# Tests Menu +tests.main.title=🩺 My Tests\n\nPlease choose a category: +tests.main.btn=View Tests +tests.main.list=Test Categories +tests.main.opt1=Fever Panel +tests.main.opt2=Heart Health +tests.main.opt3=Joint & Bone +tests.main.opt4=Master Health +tests.main.opt5=Pregnancy +tests.main.opt6=Sugar Control +tests.main.opt7=Women's Wellness + +tests.fever.title=🌡️ Fever Panel Tests +tests.fever.btn=View Tests +tests.fever.list=Tests +tests.fever.opt1=Fever Ultra Panel +tests.fever.opt2=CBC +tests.fever.opt3=IgE Total Serum +tests.fever.opt4=LFT +tests.fever.opt5=Stool Occult Blood +tests.fever.desc1=₹1,999 (11 Tests) +tests.fever.desc2=₹300 +tests.fever.desc3=₹400 +tests.fever.desc4=₹500 +tests.fever.desc5=₹250 + +tests.heart.title=❤️ Heart Health Packages +tests.heart.btn=View Packages +tests.heart.list=Packages +tests.heart.opt1=Heart Health Plus +tests.heart.opt2=Amaya Int Cardiac +tests.heart.opt3=Cardiac Pkg - Amaya +tests.heart.opt4=Healthy Heart Basic +tests.heart.opt5=Heart Basic +tests.heart.opt6=Heart Master +tests.heart.opt7=Hypertension Comp + +tests.joint.title=🦴 Joint & Bone Packages +tests.joint.opt1=Joint Care Plus +tests.joint.opt2=Arthritis Package + +tests.master.title=🩺 Master Health Check Plans +tests.master.btn=View Plans +tests.master.list=Plans +tests.master.opt1=Master Health Basic +tests.master.opt2=Master Health Exec +tests.master.opt3=Master Health Prem + +tests.preg.title=🤰 Pregnancy Packages +tests.preg.opt1=Pregnancy Basic +tests.preg.opt2=Pregnancy Advanced + +tests.sugar.title=🩸 Sugar Control Series +tests.sugar.btn=View Packages +tests.sugar.list=Packages +tests.sugar.opt1=Sugar Control Basic +tests.sugar.opt2=Sugar Control Plus +tests.sugar.opt3=Diabetes Monitoring +tests.sugar.opt4=HbA1c Comprehensive + +tests.women.title=👩 Women's Wellness Plans +tests.women.opt1=Wellness Basic +tests.women.opt2=Wellness Advanced + +# Doctor Menu +doc.main.title=🩺 Doctor Appointments +doc.main.btn=View Options +doc.main.list=Options +doc.main.opt1=Book New Appointment +doc.main.opt2=View Appointments +doc.main.opt3=Reschedule Appointment +doc.main.opt4=Cancel Appointment + +doc.dept.title=Please select a department. +doc.dept.btn=View Departments +doc.dept.list=Departments +doc.dept.opt1=General Medicine +doc.dept.opt2=Cardiology +doc.dept.opt3=Orthopedics +doc.dept.opt4=Neurology +doc.dept.opt5=Pediatrics +doc.dept.opt6=Gynecology +doc.dept.opt7=Dermatology +doc.dept.opt8=ENT +doc.dept.opt9=Ophthalmology + +doc.sel.title=Available Doctors +doc.sel.btn=Select Doctor +doc.sel.list=Doctors + +doc.date.title=Available Dates +doc.date.btn=Select Date +doc.date.list=Dates + +doc.time.title=Available Time Slots +doc.time.btn=Select Time +doc.time.list=Time Slots + +doc.summary.title=Appointment Summary +doc.summary.btn=Select Option +doc.summary.list=Options +doc.summary.confirm=Confirm Booking +doc.summary.change=Change Time +doc.summary.cancel=Cancel + +doc.reschedule.ask=Please enter your Appointment ID. +doc.cancel.ask=Please enter your Appointment ID. +doc.no_apps=You have no upcoming appointments. + +# Confirmations +booking.service.confirm=✅ Booking Confirmed\nService: {0}\nAmount: {1}\n\nPlease share:\n📍 Service Address\nor\n📍 Live Location +booking.ask_address=Please enter your address. +booking.ask_date=Thank you.\nPlease reply with your preferred date.\n\n📅 DD/MM/YYYY +booking.ask_time.title=Please choose the preferred start time. +booking.ask_time.btn=Select Time +booking.ask_time.list=Available Slots +booking.ask_time.opt1=9 AM - 1 PM +booking.ask_time.opt2=1 PM - 5 PM +booking.ask_time.opt3=5 PM - 9 PM + +booking.service.success=✅ Service Booked Successfully\nBooking ID:\n{0}\nPatient ID:\n{1}\n\nPlease provide your Patient ID at the hospital billing desk for payment.\n\nOur nursing team will contact you shortly.\nThank you for choosing Rajalakshmi Medical College. + +booking.test.confirm_pkg=Selected Package\n\n{0}\nPrice:\n{1}\nWould you like to continue? +booking.test.btn_book=Book Now +booking.test.btn_back=Back +booking.test.ask_coll=How would you like to provide your sample? +booking.test.coll_home=Home Collection +booking.test.coll_visit=Visit Laboratory +booking.test.ask_date=Please reply with your preferred date.\n📅 DD/MM/YYYY +booking.test.ask_time.title=Choose your preferred time slot. +booking.test.ask_time.btn=Select Slot +booking.test.ask_time.list=Time Slots +booking.test.opt1=7 AM - 9 AM +booking.test.opt2=9 AM - 11 AM +booking.test.opt3=11 AM - 1 PM + +booking.test.success=✅ Booking Successful\nBooking ID:\n{0}\nPatient ID:\n{1}\n\nPackage:\n{2}\nAmount:\n{3}\n\nPlease provide your Patient ID at the hospital billing desk for payment.\n\nCollection:\n{4}\nTechnician Visit\n{5}\n{6}\n\nThank you for choosing Rajalakshmi Medical College. + +# Final Main Menu +menu.final.title=Would you like to go back to the Main Menu? +menu.final.yes=Main Menu +menu.final.no=Exit + +doc.booking.success=✅ Appointment Booked Successfully\n\nAppointment ID:\n{0}\n\nPatient ID:\n{1}\n\nPlease provide your Patient ID at the hospital billing desk for payment.\n\nDoctor:\n{2}\n\nDepartment:\n{3}\n\nDate:\n{4}\n\nTime:\n{5}\n\nPlease arrive 15 minutes before your appointment.\nThank you for choosing Rajalakshmi Medical College +doc.view_apps.title=Your Upcoming Appointments +doc.view_apps.item=Appointment ID:\n{0}\nDoctor:\n{1}\nDepartment:\n{2}\nDate:\n{3}\nTime:\n{4}\nStatus:\n✅ {5} +doc.view_apps.btn=Select Option +doc.view_apps.list=Options +doc.view_apps.opt1=Cancel Appointment +doc.view_apps.opt2=Reschedule +doc.view_apps.opt3=Main Menu +doc.invalid_id=Invalid Appointment ID. Please try again. +doc.reschedule.time.title=Available New Time Slots +doc.reschedule.time.btn=Select Time +doc.reschedule.time.list=Time Slots +doc.reschedule.success=✅ Appointment Rescheduled\nDoctor:\n{0}\nNew Time:\n{1}\nDate:\n{2} +error.generic=Error. Please try again. +doc.cancel.confirm_msg=Are you sure you want to cancel? +doc.cancel.yes=Yes +doc.cancel.no=No +doc.cancel.success=✅ Appointment Cancelled Successfully. + +pagination.next=Next ➡️ +# Services Menu +services.nursing.title=💉 Nursing Services\n\nChoose: +services.nursing.opt1=Male Nurse +services.nursing.opt2=Female Nurse +services.attender.title=🤝 Attender Services +services.attender.opt1=Male Attender +services.attender.opt2=Female Attender +services.physio.title=🏃 Physiotherapy\n\nPrice: ₹700\n\nWould you like to book? +services.physio.yes=Yes +services.physio.no=No +services.medproc.title=🩺 Medical Procedures +services.medproc.btn=View Options +services.medproc.list=Procedures +services.medproc.opt1=IM Injection +services.medproc.opt2=IV Injection +services.medproc.opt3=Nebulization +services.medproc.opt4=Dressing Charges +services.medproc.opt5=Catheterization +services.digital.title=💻 Digital Consultation\n\nConsultation Fee: ₹750\n\nWould you like to schedule? +services.digital.yes=Yes +services.digital.back=Back +services.homecare.title=🏡 Home Care Packages +services.homecare.btn=View Services +services.homecare.list=Services +services.homecare.opt1=Nursing Care +services.homecare.opt2=Attender Care +services.homecare.opt3=Physiotherapy +services.homecare.opt4=Medical Procedures +services.main.title=🩺 My Services\n\nPlease choose a category: +services.main.btn=View Services +services.main.list=Service Categories +services.main.opt1=Nursing Services +services.main.opt2=Attender Services +services.main.opt3=Physiotherapy +services.main.opt4=Medical Procedures +services.main.opt5=Digital Consult +services.main.opt6=Home Care Packages + +new.menu.title=How may I help you today? +new.menu.opt1=🚨 Emergency Contact +new.menu.opt2=👨‍⚕️ Doctor Appointment +new.menu.opt3=🏥 Health Packages +new.menu.opt4=🔬 Book Lab Test +new.menu.opt5=💼 Job Enquiry +new.menu.opt6=📞 Assistance +new.menu.btn=Select Option +new.menu.list=Main Menu + +new.emergency.text=For emergency assistance, please contact our emergency department immediately.\n📞 Emergency Contact: +91 44 2715 2000 +new.btn.main_menu=Main Menu +new.btn.exit=Exit +new.btn.back=⬅️ Back +new.exit.message=Thank you for contacting Rajalakshmi Medical College. Have a healthy day! 👋 + +new.assistance.text=Certainly. For assistance, you can contact our reception team.\n📞 Reception Contact Number: +91 44 2715 2000 + +new.job.ask_phone=Please reply with your 10-digit Contact Number. +new.job.invalid_phone=Invalid phone number. Please enter a 10-digit number. +new.job.ask_email=Thank you. Now please reply with your Email ID. +new.job.invalid_email=Invalid email format. Please enter a valid Email ID. +new.job.success=Thank you. Your details have been received successfully.\nOur recruitment team will contact you regarding suitable opportunities.\n📞 HR Contact: +91 44 2715 2000 (hr@rmchospital.com) + +new.doc.dept.title=Please select a speciality. +new.doc.dept.btn=View Specialities +new.doc.dept.list=Specialities +new.doc.sel.title=Please select a Doctor. +new.doc.sel.btn=View Doctors +new.doc.sel.list=Doctors +new.doc.ask_date=Please enter your preferred visit date in DD/MM/YYYY format. +new.doc.invalid_date=Invalid date format or date is invalid. Please reply with a valid date in DD/MM/YYYY format. +new.doc.time.title=Please choose an available time slot. +new.doc.time.btn=View Time Slots +new.doc.time.list=Time Slots +new.doc.summary=👨‍⚕️ Doctor: {0}\n🏥 Speciality: {1}\n📅 Date: {2}\n⏰ Time: {3} +new.doc.confirm_btn=Confirm Appointment +new.doc.cancel_btn=Cancel +new.doc.success=✅ Your appointment has been successfully booked.\n\nDoctor: {0}\nSpeciality: {1}\nDate: {2}\nTime: {3}\nAppointment ID: {4} + +new.pkg.title=Please select a Healthcare Package. +new.pkg.btn=View Packages +new.pkg.list=Packages +new.pkg.opt1=Gen Health (₹1500) +new.pkg.opt2=Cardiac Pkg (₹3000) +new.pkg.opt3=Maternity (₹2500) +new.pkg.ask_coupon=Do you have a coupon code? Please type the code below, or click 'Skip Coupon' to proceed. +new.pkg.btn_skip=Skip Coupon +new.pkg.invalid_coupon=Invalid coupon code. Please try again or click 'Skip Coupon'. +new.pkg.ask_date=Please enter your preferred date in DD/MM/YYYY format. +new.pkg.ask_time=Please select a preferred time slot. +new.pkg.confirm_title=Please confirm your booking details: +new.pkg.summary=📦 Package: {0}\n📅 Date: {1}\n⏰ Time: {2}\n💵 Rate: {3} (Coupon: {4}) +new.pkg.success=✅ Healthcare package scheduled successfully.\n\nPackage: {0}\nDate: {1}\nTime: {2}\nFinal Rate: {3}\nBooking ID: {4} + +new.inv.title=Please choose the investigation type. +new.inv.btn=Choose Type +new.inv.list=Types +new.inv.opt1=🩸 Blood / Lab +new.inv.opt2=🩻 Radiology + +new.blood.title=Please select a blood test. +new.blood.btn=View Tests +new.blood.list=Tests +new.blood.opt1=CBC Test (₹300) +new.blood.opt2=LFT Test (₹500) +new.blood.opt3=Lipid Profile (₹450) +new.blood.opt4=HbA1c Test (₹350) +new.blood.confirm=You have selected:\n🧪 {0}\nRate: {1}\nWould you like to confirm? +new.blood.btn_confirm=Confirm +new.blood.btn_cancel=Cancel + +new.radio.title=Please select a Radiology service. +new.radio.btn=View Services +new.radio.list=Services +new.radio.opt1=X-Ray (₹500) +new.radio.opt2=CT Scan (₹2500) +new.radio.opt3=MRI (₹5000) +new.radio.opt4=Ultrasound (₹1200) + +new.btn.confirm=Confirm Booking +new.btn.cancel=Cancel +new.booking.success=✅ Booking scheduled successfully.\n\nService: {0}\nDate: {1}\nTime: {2}\nRate: {3}\nBooking ID: {4} diff --git a/src/main/resources/messages_hi.properties b/src/main/resources/messages_hi.properties new file mode 100644 index 0000000..488f9b2 --- /dev/null +++ b/src/main/resources/messages_hi.properties @@ -0,0 +1,110 @@ +welcome.body=आरएमसी अस्पताल में आपका स्वागत है! कृपया अपनी पसंदीदा भाषा चुनें। +menu.btn.services=मेरी सेवाएं +menu.btn.tests=मेरे परीक्षण +menu.btn.booking=अपॉइंटमेंट बुक करें +invalid.option=अमान्य विकल्प। कृपया पुनः प्रयास करें। + +new.menu.title=आज मैं आपकी क्या सहायता कर सकता हूँ? +new.menu.opt1=🚨 आपातकालीन संपर्क +new.menu.opt2=👨‍⚕️ डॉक्टर अपॉइंटमेंट +new.menu.opt3=🏥 स्वास्थ्य पैकेज +new.menu.opt4=🔬 जांच अपॉइंटमेंट +new.menu.opt5=💼 नौकरी पूछताछ +new.menu.opt6=📞 सहायता +new.menu.btn=विकल्प चुनें +new.menu.list=मुख्य सूची + +new.emergency.text=आपातकालीन सहायता के लिए, कृपया तुरंत हमारे आपातकालीन विभाग से संपर्क करें।\n📞 आपातकालीन संपर्क: +91 44 2715 2000 +new.btn.main_menu=मुख्य सूची +new.btn.exit=बाहर निकलें +new.btn.back=⬅️ वापस +new.exit.message=राजलक्ष्मी मेडिकल कॉलेज से संपर्क करने के लिए धन्यवाद। आपका दिन स्वस्थ रहे! 👋 + +new.assistance.text=बिल्कुल। सहायता के लिए, आप हमारी रिसेप्शन टीम से संपर्क कर सकते हैं।\n📞 रिसेप्शन संपर्क नंबर: +91 44 2715 2000 + +new.job.ask_phone=कृपया अपना 10 अंकों का संपर्क नंबर दर्ज करें। +new.job.invalid_phone=अमान्य फ़ोन नंबर। कृपया 10 अंकों का नंबर दर्ज करें। +new.job.ask_email=धन्यवाद। अब कृपया अपना ईमेल आईडी दर्ज करें। +new.job.invalid_email=अमान्य ईमेल प्रारूप। कृपया एक मान्य ईमेल आईडी दर्ज करें। +new.job.success=धन्यवाद। आपके विवरण सफलतापूर्वक प्राप्त हो गए हैं।\nहमारी भर्ती टीम उपयुक्त अवसरों के संबंध में आपसे संपर्क करेगी।\n📞 मानव संसाधन (HR) संपर्क: +91 44 2715 2000 (hr@rmchospital.com) + +new.doc.dept.title=कृपया एक विशेषता चुनें। +new.doc.dept.btn=विशेषताएं देखें +new.doc.dept.list=विशेषताएं +new.doc.sel.title=कृपया एक डॉक्टर चुनें। +new.doc.sel.btn=डॉक्टर देखें +new.doc.sel.list=डॉक्टर +new.doc.ask_date=कृपया अपनी पसंदीदा यात्रा तिथि DD/MM/YYYY प्रारूप में दर्ज करें। +new.doc.invalid_date=अमान्य तिथि प्रारूप या तिथि अमान्य है। कृपया DD/MM/YYYY प्रारूप में एक मान्य तिथि दर्ज करें। +new.doc.time.title=कृपया एक उपलब्ध समय स्लॉट चुनें। +new.doc.time.btn=समय स्लॉट देखें +new.doc.time.list=समय स्लॉट +new.doc.summary=👨‍⚕️ डॉक्टर: {0}\n🏥 विभाग: {1}\n📅 तिथि: {2}\n⏰ समय: {3} +new.doc.confirm_btn=अपॉइंटमेंट की पुष्टि करें +new.doc.cancel_btn=रद्द करें +new.doc.success=✅ आपका अपॉइंटमेंट सफलतापूर्वक बुक हो गया है।\n\nडॉक्टर: {0}\nविभाग: {1}\nतिथि: {2}\nसमय: {3}\nअपॉइंटमेंट आईडी: {4} + +new.pkg.title=कृपया एक स्वास्थ्य पैकेज चुनें। +new.pkg.btn=पैकेज देखें +new.pkg.list=पैकेज +new.pkg.opt1=सामान्य स्वास्थ्य (₹1500) +new.pkg.opt2=हृदय पैकेज (₹3000) +new.pkg.opt3=मातृत्व पैकेज (₹2500) +new.pkg.ask_coupon=क्या आपके पास कूपन कोड है? कृपया कूपन कोड दर्ज करें, या आगे बढ़ने के लिए 'कूपन छोड़ें' पर क्लिक करें। +new.pkg.btn_skip=कूपन छोड़ें +new.pkg.invalid_coupon=अमान्य कूपन कोड। कृपया पुनः प्रयास करें या 'कूपन छोड़ें' पर क्लिक करें। +new.pkg.ask_date=कृपया पसंदीदा तिथि DD/MM/YYYY प्रारूप में दर्ज करें। +new.pkg.ask_time=कृपया पसंदीदा समय स्लॉट चुनें। +new.pkg.confirm_title=कृपया अपने बुकिंग विवरण की पुष्टि करें: +new.pkg.summary=📦 पैकेज: {0}\n📅 तिथि: {1}\n⏰ समय: {2}\n💵 दर: {3} (कूपन: {4}) +new.pkg.success=✅ स्वास्थ्य पैकेज सफलतापूर्वक निर्धारित किया गया है।\n\nपैकेज: {0}\nतिथि: {1}\nसमय: {2}\nअंतिम दर: {3}\nबुकिंग आईडी: {4} + +new.inv.title=कृपया जांच का प्रकार चुनें। +new.inv.btn=प्रकार चुनें +new.inv.list=प्रकार +new.inv.opt1=🩸 रक्त / लैब +new.inv.opt2=🩻 रेडियोलॉजी + +new.blood.title=कृपया रक्त परीक्षण चुनें। +new.blood.btn=परीक्षण देखें +new.blood.list=परीक्षण +new.blood.opt1=CBC रक्त जांच (₹300) +new.blood.opt2=LFT लीवर जांच (₹500) +new.blood.opt3=लिपिड प्रोफाइल (₹450) +new.blood.opt4=HbA1c शुगर जांच (₹350) +new.blood.confirm=आपने चुना है:\n🧪 {0}\nदर: {1}\nक्या आप पुष्टि करना चाहते हैं? +new.blood.btn_confirm=पुष्टि करें +new.blood.btn_cancel=रद्द करें + +new.radio.title=कृपया रेडियोलॉजी सेवा चुनें। +new.radio.btn=सेवाएं देखें +new.radio.list=सेवाएं +new.radio.opt1=एक्स-रे (₹500) +new.radio.opt2=सीटी स्कैन (₹2500) +new.radio.opt3=एमआरआई (₹5000) +new.radio.opt4=अल्ट्रासाउंड (₹1200) + +new.btn.confirm=बुकिंग की पुष्टि करें +new.btn.cancel=रद्द करें +new.booking.success=✅ बुकिंग सफलतापूर्वक निर्धारित की गई है।\n\nसेवा: {0}\nतिथि: {1}\nसमय: {2}\nदर: {3}\nबुकिंग आईडी: {4} + +doc.dept.title=कृपया एक विभाग चुनें। +doc.dept.btn=विभाग देखें +doc.dept.list=विभाग +doc.dept.opt1=सामान्य चिकित्सा +doc.dept.opt2=हृदय रोग विज्ञान +doc.dept.opt3=हड्डी रोग विज्ञान +doc.dept.opt4=न्यूरोलॉजी +doc.dept.opt5=बाल रोग विज्ञान +doc.dept.opt6=स्त्री रोग विज्ञान +doc.dept.opt7=त्वचा विज्ञान +doc.dept.opt8=ईएनटी +doc.dept.opt9=नेत्र विज्ञान + +booking.test.ask_date=कृपया अपनी पसंदीदा तिथि दर्ज करें।\n📅 DD/MM/YYYY +booking.test.ask_time.title=अपना पसंदीदा समय स्लॉट चुनें। +booking.test.ask_time.btn=स्लॉट चुनें +booking.test.ask_time.list=समय स्लॉट +booking.test.opt1=सुबह 7 - 9 बजे +booking.test.opt2=सुबह 9 - 11 बजे +booking.test.opt3=सुबह 11 - दोपहर 1 बजे diff --git a/src/main/resources/messages_ta.properties b/src/main/resources/messages_ta.properties new file mode 100644 index 0000000..974e73c --- /dev/null +++ b/src/main/resources/messages_ta.properties @@ -0,0 +1,293 @@ +welcome.body=👋 ராஜலட்சுமி மருத்துவக் கல்லூரிக்கு வரவேற்கிறோம்! நான் உங்கள் மெய்நிகர் சுகாதார உதவியாளர். தயவுசெய்து கீழ்க்கண்ட விருப்பங்களில் ஒன்றைத் தேர்ந்தெடுக்கவும்: +menu.btn.services=என் சேவைகள் +menu.btn.tests=பரிசோதனைகள் +menu.btn.booking=முன்பதிவு செய்க +menu.services=🩺 எனது சேவைகள்\n\nதயவுசெய்து ஒரு வகையைத் தேர்ந்தெடுக்கவும்:\n1️⃣ செவிலியர் சேவைகள்\n2️⃣ உதவியாளர் சேவைகள்\n3️⃣ பிசியோதெரபி\n4️⃣ மருத்துவ நடைமுறைகள்\n5️⃣ டிஜிட்டல் ஆலோசனை\n6️⃣ வீட்டு பராமரிப்பு தொகுப்புகள்\n\nவிருப்ப எண்ணுடன் பதிலளிக்கவும். +menu.tests=🏥 ராஜலட்சுமி மருத்துவக் கல்லூரி\nஆய்வக சேவைகளுக்கு வரவேற்கிறோம்.\n\nசுகாதாரத் தொகுப்பு வகையைத் தேர்ந்தெடுக்கவும்.\n1️⃣ காய்ச்சல் பரிசோதனைகள்\n2️⃣ இதய ஆரோக்கியம்\n3️⃣ மூட்டு மற்றும் எலும்பு\n4️⃣ முழுமையான உடல் பரிசோதனை\n5️⃣ கர்ப்ப காலம்\n6️⃣ சர்க்கரை கட்டுப்பாடு\n7️⃣ பெண்கள் நல்வாழ்வு\n8️⃣ பரிசோதனையைத் தேடுங்கள்\n\nவிருப்ப எண்ணுடன் பதிலளிக்கவும். +menu.doctor=👨‍⚕️ மருத்துவர் சந்திப்பு\nதயவுசெய்து ஒரு விருப்பத்தைத் தேர்ந்தெடுக்கவும்\n1️⃣ புதிய சந்திப்பை முன்பதிவு செய்ய\n2️⃣ எனது சந்திப்புகளைப் பார்க்க\n3️⃣ சந்திப்பை மாற்றியமைக்க\n4️⃣ சந்திப்பை ரத்து செய்ய\n\nவிருப்ப எண்ணுடன் பதிலளிக்கவும். +invalid.option=தவறான விருப்பம். தயவுசெய்து மீண்டும் முயற்சிக்கவும். + +# Tests Menu +tests.main.title=🩺 எனது பரிசோதனைகள்\n\nதயவுசெய்து ஒரு வகையைத் தேர்ந்தெடுக்கவும்: +tests.main.btn=பரிசோதனைகளை காண்க +tests.main.list=பரிசோதனை பிரிவுகள் +tests.main.opt1=காய்ச்சல் பரிசோதனை +tests.main.opt2=இதய ஆரோக்கியம் +tests.main.opt3=மூட்டு & எலும்பு +tests.main.opt4=முழு உடல் பரிசோதனை +tests.main.opt5=கர்ப்ப காலம் +tests.main.opt6=சர்க்கரை கட்டுப்பாடு +tests.main.opt7=பெண்கள் நல்வாழ்வு + +tests.fever.title=🌡️ காய்ச்சல் பரிசோதனைகள் +tests.fever.btn=பரிசோதனைகளை காண்க +tests.fever.list=பரிசோதனைகள் +tests.fever.opt1=காய்ச்சல் அல்ட்ரா +tests.fever.opt2=CBC +tests.fever.opt3=IgE Total Serum +tests.fever.opt4=LFT +tests.fever.opt5=மலம் பரிசோதனை +tests.fever.desc1=₹1,999 (11 பரிசோதனைகள்) +tests.fever.desc2=₹300 +tests.fever.desc3=₹400 +tests.fever.desc4=₹500 +tests.fever.desc5=₹250 + +tests.heart.title=❤️ இதய ஆரோக்கிய தொகுப்புகள் +tests.heart.btn=தொகுப்புகளை காண்க +tests.heart.list=தொகுப்புகள் +tests.heart.opt1=ஹார்ட் ஹெல்த் பிளஸ் +tests.heart.opt2=அமயா கார்டியாக் +tests.heart.opt3=அமயா பேக்கேஜ் +tests.heart.opt4=ஹெல்தி ஹார்ட் பேசிக் +tests.heart.opt5=ஹார்ட் பேசிக் +tests.heart.opt6=ஹார்ட் மாஸ்டர் +tests.heart.opt7=உயர் ரத்த அழுத்தம் + +tests.joint.title=🦴 மூட்டு மற்றும் எலும்பு தொகுப்புகள் +tests.joint.opt1=ஜாயின்ட் கேர் பிளஸ் +tests.joint.opt2=ஆர்த்ரிடிஸ் பேக்கேஜ் + +tests.master.title=🩺 முழுமையான உடல் பரிசோதனை திட்டங்கள் +tests.master.btn=திட்டங்களை காண்க +tests.master.list=திட்டங்கள் +tests.master.opt1=மாஸ்டர் பேசிக் +tests.master.opt2=மாஸ்டர் எக்ஸிகியூட் +tests.master.opt3=மாஸ்டர் பிரீமியம் + +tests.preg.title=🤰 கர்ப்ப கால தொகுப்புகள் +tests.preg.opt1=கர்ப்ப காலம் பேசிக் +tests.preg.opt2=கர்ப்ப அட்வான்ஸ்டு + +tests.sugar.title=🩸 சர்க்கரை கட்டுப்பாடு தொடர் +tests.sugar.btn=தொகுப்புகளை காண்க +tests.sugar.list=தொகுப்புகள் +tests.sugar.opt1=சர்க்கரை பேசிக் +tests.sugar.opt2=சர்க்கரை பிளஸ் +tests.sugar.opt3=நீரிழிவு கண்காணிப்பு +tests.sugar.opt4=HbA1c விரிவானது + +tests.women.title=👩 பெண்கள் நல்வாழ்வு திட்டங்கள் +tests.women.opt1=பெண்கள் பேசிக் +tests.women.opt2=பெண்கள் அட்வான்ஸ்டு + +# Doctor Menu +doc.main.title=🩺 மருத்துவர் சந்திப்புகள் +doc.main.btn=விருப்பங்களை காண்க +doc.main.list=விருப்பங்கள் +doc.main.opt1=புதிய முன்பதிவு +doc.main.opt2=சந்திப்புகளை பார் +doc.main.opt3=மாற்றியமைக்க +doc.main.opt4=ரத்து செய்ய + +doc.dept.title=தயவுசெய்து ஒரு துறையை தேர்ந்தெடுக்கவும். +doc.dept.btn=துறைகளை காண்க +doc.dept.list=துறைகள் +doc.dept.opt1=பொது மருத்துவம் +doc.dept.opt2=இருதயவியல் +doc.dept.opt3=எலும்பியல் +doc.dept.opt4=நரம்பியல் +doc.dept.opt5=குழந்தை மருத்துவம் +doc.dept.opt6=மகப்பேறியல் +doc.dept.opt7=தோல் மருத்துவம் +doc.dept.opt8=காது மூக்கு தொண்டை +doc.dept.opt9=கண் மருத்துவம் + +doc.sel.title=கிடைக்கும் மருத்துவர்கள் +doc.sel.btn=மருத்துவரை தேர்ந்தெடுங்கள் +doc.sel.list=மருத்துவர்கள் + +doc.date.title=கிடைக்கும் தேதிகள் +doc.date.btn=தேதியை தேர்ந்தெடுங்கள் +doc.date.list=தேதிகள் + +doc.time.title=கிடைக்கும் நேரங்கள் +doc.time.btn=நேரத்தை தேர்ந்தெடுங்கள் +doc.time.list=நேரங்கள் + +doc.summary.title=சந்திப்பு சுருக்கம் +doc.summary.btn=விருப்பத்தை தேர்ந்தெடுங்கள் +doc.summary.list=விருப்பங்கள் +doc.summary.confirm=உறுதிப்படுத்துக +doc.summary.change=நேரத்தை மாற்றவும் +doc.summary.cancel=ரத்து செய்யவும் + +doc.reschedule.ask=தயவுசெய்து உங்கள் சந்திப்பு ஐடியை உள்ளிடவும். +doc.cancel.ask=தயவுசெய்து உங்கள் சந்திப்பு ஐடியை உள்ளிடவும். +doc.no_apps=உங்களுக்கு வரவிருக்கும் சந்திப்புகள் ஏதும் இல்லை. + +# Confirmations +booking.service.confirm=✅ முன்பதிவு உறுதி செய்யப்பட்டது\nசேவை: {0}\nகட்டணம்: {1}\n\nதயவுசெய்து பகிரவும்:\n📍 சேவை முகவரி\nஅல்லது\n📍 நேரலை இருப்பிடம் (Live Location) +booking.ask_address=தயவுசெய்து உங்கள் முகவரியை உள்ளிடவும். +booking.ask_date=நன்றி.\nஉங்கள் விருப்பமான தேதியை உள்ளிடவும்.\n\n📅 DD/MM/YYYY +booking.ask_time.title=விருப்பமான தொடக்க நேரத்தை தேர்வு செய்யவும். +booking.ask_time.btn=நேரத்தை தேர்ந்தெடுக்கவும் +booking.ask_time.list=கிடைக்கும் நேரங்கள் +booking.ask_time.opt1=காலை 9 - மதியம் 1 +booking.ask_time.opt2=மதியம் 1 - மாலை 5 +booking.ask_time.opt3=மாலை 5 - இரவு 9 + +booking.service.success=✅ சேவை வெற்றிகரமாக முன்பதிவு செய்யப்பட்டது\nமுன்பதிவு எண்:\n{0}\nநோயாளி எண்:\n{1}\n\nபணம் செலுத்த உங்கள் நோயாளி எண்ணை மருத்துவமனை பில்லிங் டெஸ்கில் வழங்கவும்.\n\nஎங்கள் செவிலியர் குழு உங்களை விரைவில் தொடர்புகொள்ளும்.\nராஜலட்சுமி மருத்துவக் கல்லூரியை தேர்ந்தெடுத்ததற்கு நன்றி. + +booking.test.confirm_pkg=தேர்ந்தெடுக்கப்பட்ட தொகுப்பு\n\n{0}\nவிலை:\n{1}\nநீங்கள் தொடர விரும்புகிறீர்களா? +booking.test.btn_book=முன்பதிவு செய் +booking.test.btn_back=பின்செல் +booking.test.ask_coll=மாதிரியை எவ்வாறு வழங்க விரும்புகிறீர்கள்? +booking.test.coll_home=வீட்டு மாதிரி சேகரிப்பு +booking.test.coll_visit=ஆய்வகத்திற்கு வருகை +booking.test.ask_date=உங்கள் விருப்பமான தேதியை உள்ளிடவும்.\n📅 DD/MM/YYYY +booking.test.ask_time.title=விருப்பமான நேரத்தை தேர்வு செய்யவும். +booking.test.ask_time.btn=நேரத்தை தேர்ந்தெடுக்கவும் +booking.test.ask_time.list=நேரங்கள் +booking.test.opt1=காலை 7 - காலை 9 +booking.test.opt2=காலை 9 - காலை 11 +booking.test.opt3=காலை 11 - மதியம் 1 + +booking.test.success=✅ முன்பதிவு வெற்றிகரமாக முடிந்தது\nமுன்பதிவு எண்:\n{0}\nநோயாளி எண்:\n{1}\n\nதொகுப்பு:\n{2}\nகட்டணம்:\n{3}\n\nபணம் செலுத்த உங்கள் நோயாளி எண்ணை மருத்துவமனை பில்லிங் டெஸ்கில் வழங்கவும்.\n\nசேகரிப்பு:\n{4}\nதொழில்நுட்ப வல்லுநர் வருகை\n{5}\n{6}\n\nராஜலட்சுமி மருத்துவக் கல்லூரியை தேர்ந்தெடுத்ததற்கு நன்றி. + +# Final Main Menu +menu.final.title=நீங்கள் பிரதான மெனுவுக்குச் செல்ல விரும்புகிறீர்களா? +menu.final.yes=பிரதான மெனு +menu.final.no=வெளியேறு + +doc.booking.success=✅ சந்திப்பு வெற்றிகரமாக முன்பதிவு செய்யப்பட்டது\n\nசந்திப்பு ஐடி:\n{0}\n\nநோயாளி ஐடி:\n{1}\n\nபணம் செலுத்த உங்கள் நோயாளி எண்ணை மருத்துவமனை பில்லிங் டெஸ்கில் வழங்கவும்.\n\nமருத்துவர்:\n{2}\n\nதுறை:\n{3}\n\nதேதி:\n{4}\n\nநேரம்:\n{5}\n\nஉங்கள் சந்திப்புக்கு 15 நிமிடங்களுக்கு முன்பு வரவும்.\nராஜலட்சுமி மருத்துவக் கல்லூரியை தேர்ந்தெடுத்ததற்கு நன்றி +doc.view_apps.title=உங்கள் வரவிருக்கும் சந்திப்புகள் +doc.view_apps.item=சந்திப்பு ஐடி:\n{0}\nமருத்துவர்:\n{1}\nதுறை:\n{2}\nதேதி:\n{3}\nநேரம்:\n{4}\nநிலை:\n✅ {5} +doc.view_apps.btn=விருப்பத்தை தேர்ந்தெடுக்கவும் +doc.view_apps.list=விருப்பங்கள் +doc.view_apps.opt1=ரத்து செய்யவும் +doc.view_apps.opt2=மாற்றியமைக்க +doc.view_apps.opt3=பிரதான மெனு +doc.invalid_id=தவறான சந்திப்பு ஐடி. தயவுசெய்து மீண்டும் முயற்சிக்கவும். +doc.reschedule.time.title=கிடைக்கும் புதிய நேரங்கள் +doc.reschedule.time.btn=நேரத்தை தேர்ந்தெடுக்கவும் +doc.reschedule.time.list=நேரங்கள் +doc.reschedule.success=✅ சந்திப்பு மாற்றியமைக்கப்பட்டது\nமருத்துவர்:\n{0}\nபுதிய நேரம்:\n{1}\nதேதி:\n{2} +error.generic=பிழை. தயவுசெய்து மீண்டும் முயற்சிக்கவும். +doc.cancel.confirm_msg=நிச்சயமாக ரத்து செய்ய விரும்புகிறீர்களா? +doc.cancel.yes=ஆம் +doc.cancel.no=இல்லை +doc.cancel.success=✅ சந்திப்பு வெற்றிகரமாக ரத்து செய்யப்பட்டது. + +pagination.next=அடுத்து ➡️ +# Services Menu +services.nursing.title=💉 செவிலியர் சேவைகள்\n\nதேர்வு செய்க: +services.nursing.opt1=ஆண் செவிலியர் +services.nursing.opt2=பெண் செவிலியர் +services.attender.title=🤝 உதவியாளர் சேவைகள் +services.attender.opt1=ஆண் உதவியாளர் +services.attender.opt2=பெண் உதவியாளர் +services.physio.title=🏃 பிசியோதெரபி\n\nவிலை: ₹700\n\nநீங்கள் முன்பதிவு செய்ய விரும்புகிறீர்களா? +services.physio.yes=ஆம் +services.physio.no=இல்லை +services.medproc.title=🩺 மருத்துவ நடைமுறைகள் +services.medproc.btn=விருப்பங்களை காண்க +services.medproc.list=நடைமுறைகள் +services.medproc.opt1=IM ஊசி +services.medproc.opt2=IV ஊசி +services.medproc.opt3=நெபுலைசேஷன் +services.medproc.opt4=கட்டு போடுதல் +services.medproc.opt5=வடிகுழாய் +services.digital.title=💻 டிஜிட்டல் ஆலோசனை\n\nஆலோசனை கட்டணம்: ₹750\n\nநீங்கள் முன்பதிவு செய்ய விரும்புகிறீர்களா? +services.digital.yes=ஆம் +services.digital.back=பின்செல் +services.homecare.title=🏡 வீட்டு பராமரிப்பு தொகுப்புகள் +services.homecare.btn=சேவைகளை காண்க +services.homecare.list=சேவைகள் +services.homecare.opt1=செவிலியர் பராமரிப்பு +services.homecare.opt2=உதவியாளர் பராமரிப்பு +services.homecare.opt3=பிசியோதெரபி +services.homecare.opt4=மருத்துவ நடைமுறைகள் +services.main.title=🩺 எனது சேவைகள்\n\nதயவுசெய்து ஒரு வகையைத் தேர்ந்தெடுக்கவும்: +services.main.btn=சேவைகளை காண்க +services.main.list=சேவை பிரிவுகள் +services.main.opt1=செவிலியர் சேவைகள் +services.main.opt2=உதவியாளர் சேவைகள் +services.main.opt3=பிசியோதெரபி +services.main.opt4=மருத்துவ நடைமுறைகள் +services.main.opt5=டிஜிட்டல் ஆலோசனை +services.main.opt6=வீட்டு பராமரிப்பு + +new.menu.title=இன்று நான் உங்களுக்கு எவ்வாறு உதவ முடியும்? +new.menu.opt1=🚨 அவசர தொடர்பு எண் +new.menu.opt2=👨‍⚕️ மருத்துவர் சந்திப்பு +new.menu.opt3=🏥 சுகாதார தொகுப்புகள் +new.menu.opt4=🔬 பரிசோதனை முன்பதிவு +new.menu.opt5=💼 வேலை வாய்ப்பு +new.menu.opt6=📞 உதவி +new.menu.btn=விருப்பத்தைத் தேர்ந்தெடுக்கவும் +new.menu.list=முதன்மை மெனு + +new.emergency.text=அவசर உதவிக்கு, உடனடியாக எங்கள் அவசர சிகிச்சை பிரிவைத் தொடர்பு கொள்ளவும்.\n📞 அவசர தொடர்பு எண்: +91 44 2715 2000 +new.btn.main_menu=முதன்மை மெனு +new.btn.exit=வெளியேறு +new.btn.back=⬅️ பின்னால் +new.exit.message=ராஜலட்சுமி மருத்துவக் கல்லூரியைத் தொடர்பு கொண்டதற்கு நன்றி. ஆரோக்கியமான நாளாக அமையட்டும்! 👋 + +new.assistance.text=நிச்சயமாக. உதவிக்கு, நீங்கள் எங்கள் வரவேற்புக் குழுவைத் தொடர்பு கொள்ளலாம்.\n📞 வரவேற்பு தொடர்பு எண்: +91 44 2715 2000 + +new.job.ask_phone=தயவுசெய்து உங்கள் 10 இலக்க தொடர்பு எண்ணை உள்ளிடவும். +new.job.invalid_phone=தவறான தொலைபேசி எண். தயவுசெய்து 10 இலக்க எண்ணை உள்ளிடவும். +new.job.ask_email=நன்றி. இப்போது தயவுசெய்து உங்கள் மின்னஞ்சல் ஐடியை உள்ளிடவும். +new.job.invalid_email=தவறான மின்னஞ்சல் வடிவம். தயவுசெய்து சரியான மின்னஞ்சல் ஐடியை உள்ளிடவும். +new.job.success=நன்றி. உங்கள் विवरणங்கள் வெற்றிகரமாகப் பெறப்பட்டன.\nஎங்கள் வேலைவாய்ப்பு குழு பொருத்தமான வாய்ப்புகள் குறித்து உங்களைத் தொடர்பு கொள்ளும்.\n📞 மனிதவள (HR) தொடர்பு எண்: +91 44 2715 2000 (hr@rmchospital.com) + +new.doc.dept.title=தயவுசெய்து ஒரு சிறப்புத் துறையைத் தேர்ந்தெடுக்கவும். +new.doc.dept.btn=சிறப்புத் துறைகளைக் காண்க +new.doc.dept.list=சிறப்புத் துறைகள் +new.doc.sel.title=தயவுசெய்து ஒரு மருத்துவரைத் தேர்ந்தெடுக்கவும். +new.doc.sel.btn=மருத்துவர்களைக் காண்க +new.doc.sel.list=மருத்துவர்கள் +new.doc.ask_date=தயவுசெய்து நீங்கள் வர விரும்பும் தேதியை DD/MM/YYYY வடிவத்தில் உள்ளிடவும். +new.doc.invalid_date=தவறான தேதி வடிவம் அல்லது தேதி செல்லாதது. தயவுசெய்து DD/MM/YYYY வடிவத்தில் சரியான தேதியை உள்ளிடவும். +new.doc.time.title=தயவுசெய்து கிடைக்கக்கூடிய நேரத்தைத் தேர்ந்தெடுக்கவும். +new.doc.time.btn=நேरங்களைக் காண்க +new.doc.time.list=நேரங்கள் +new.doc.summary=👨‍⚕️ மருத்துவர்: {0}\n🏥 சிறப்புத் துறை: {1}\n📅 தேதி: {2}\n⏰ நேரம்: {3} +new.doc.confirm_btn=முன்பதிவை உறுதிசெய் +new.doc.cancel_btn=ரத்துசெய் +new.doc.success=✅ உங்கள் மருத்துவ சந்திப்பு வெற்றிகரமாக முன்பதிவு செய்யப்பட்டது.\n\nமருத்துவர்: {0}\nசிறப்புத் துறை: {1}\nதேதி: {2}\nநேரம்: {3}\nசந்திப்பு ஐடி: {4} + +new.pkg.title=தயவுசெய்து ஒரு சுகாதார தொகுப்பைத் தேர்ந்தெடுக்கவும். +new.pkg.btn=தொகுப்புகளைக் காண்க +new.pkg.list=தொகுப்புகள் +new.pkg.opt1=பொது பரிசோதனை (₹1500) +new.pkg.opt2=இதய தொகுப்பு (₹3000) +new.pkg.opt3=மகப்பேறு (₹2500) +new.pkg.ask_coupon=உங்களிடம் ஏதேனும் கூப்பன் குறியீடு உள்ளதா? கூப்பன் குறியீட்டை உள்ளிடவும் அல்லது தொடர 'கூப்பனைத் தவிர்' என்பதைத் தேர்ந்தெடுக்கவும். +new.pkg.btn_skip=கூப்பனைத் தவிர் +new.pkg.invalid_coupon=தவறான கூப்பன் குறியீடு. தயவுசெய்து மீண்டும் முயற்சிக்கவும் அல்லது 'கூப்பனைத் தவிர்' என்பதைத் தேர்ந்தெடுக்கவும். +new.pkg.ask_date=தயவுசெய்து உங்களுக்கு விருப்பமான தேதியை DD/MM/YYYY வடிவத்தில் உள்ளிடவும். +new.pkg.ask_time=தயவுசெய்து உங்களுக்கு விருப்பமான நேரத்தைத் தேர்ந்தெடுக்கவும். +new.pkg.confirm_title=முன்பதிவு விவரங்களை உறுதிப்படுத்தவும்: +new.pkg.summary=📦 தொகுப்பு: {0}\n📅 தேதி: {1}\n⏰ நேரம்: {2}\n💵 கட்டணம்: {3} (கூப்பன்: {4}) +new.pkg.success=✅ சுகாதார தொகுப்பு வெற்றிகரமாக திட்டமிடப்பட்டது.\n\nதொகுப்பு: {0}\nதேதி: {1}\nநேரம்: {2}\nஇறுதி கட்டணம்: {3}\nமுன்பதிவு ஐடி: {4} + +new.inv.title=தயவுசெய்து பரிசோதனை வகையைத் தேர்ந்தெடுக்கவும். +new.inv.btn=வகையைத் தேர்ந்தெடுக்கவும் +new.inv.list=பரிசோதனை வகைகள் +new.inv.opt1=🩸 இரத்தம் / ஆய்வகம் +new.inv.opt2=🩻 ரேடியாலजी + +new.blood.title=தயவுசெய்து இரத்தப் பரிசோதனையைத் தேர்ந்தெடுக்கவும். +new.blood.btn=பரிசோதனைகளைக் காண்க +new.blood.list=இரத்தப் பரிசோதனைகள் +new.blood.opt1=CBC ரத்தத் தேர்வு (₹300) +new.blood.opt2=LFT கல்லீரல் தேர்வு (₹500) +new.blood.opt3=லிப்பிட் தேர்வு (₹450) +new.blood.opt4=HbA1c சர்க்கரை (₹350) +new.blood.confirm=நீங்கள் தேர்ந்தெடுத்துள்ளீர்கள்:\n🧪 {0}\nகட்டணம்: {1}\nஉறுதிப்படுத்த விரும்புகிறீர்களா? +new.blood.btn_confirm=உறுதிசெய் +new.blood.btn_cancel=ரத்துசெய் + +new.radio.title=தயவுசெய்து ரேடியாலஜி சேவையைத் தேர்ந்தெடுக்கவும். +new.radio.btn=சேவைகளைக் காண்க +new.radio.list=சேவைகள் +new.radio.opt1=எக்ஸ்-ரே (₹500) +new.radio.opt2=சிடி ஸ்கேன் (₹2500) +new.radio.opt3=எம்ஆர்ஐ (₹5000) +new.radio.opt4=அல்ட்ராசவுண்ட் (₹1200) + +new.btn.confirm=முன்பதிவை உறுதிசெய் +new.btn.cancel=ரத்துசெய் +new.booking.success=✅ முன்பதிவு வெற்றிகரமாக திட்டமிடப்பட்டது.\n\nசேவை: {0}\nதேதி: {1}\nநேரம்: {2}\nகட்டணம்: {3}\nமுன்பதிவு ஐடி: {4} diff --git a/src/test/java/com/example/whatsappautomation/InactivityTimeoutTests.java b/src/test/java/com/example/whatsappautomation/InactivityTimeoutTests.java new file mode 100644 index 0000000..e6f1077 --- /dev/null +++ b/src/test/java/com/example/whatsappautomation/InactivityTimeoutTests.java @@ -0,0 +1,220 @@ +package com.example.whatsappautomation; + +import com.example.whatsappautomation.entity.UserState; +import com.example.whatsappautomation.repository.AppointmentRepository; +import com.example.whatsappautomation.repository.ServiceBookingRepository; +import com.example.whatsappautomation.repository.TestBookingRepository; +import com.example.whatsappautomation.repository.UserStateRepository; +import com.example.whatsappautomation.service.ChatbotService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.springframework.context.MessageSource; +import org.springframework.http.HttpEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.RestTemplate; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +class InactivityTimeoutTests { + + private UserStateRepository userStateRepository; + private ServiceBookingRepository serviceBookingRepository; + private TestBookingRepository testBookingRepository; + private AppointmentRepository appointmentRepository; + private MessageSource messageSource; + private RestTemplate restTemplate; + private ChatbotService chatbotService; + + @BeforeEach + void setUp() { + userStateRepository = mock(UserStateRepository.class); + serviceBookingRepository = mock(ServiceBookingRepository.class); + testBookingRepository = mock(TestBookingRepository.class); + appointmentRepository = mock(AppointmentRepository.class); + messageSource = mock(MessageSource.class); + restTemplate = mock(RestTemplate.class); + + chatbotService = new ChatbotService( + userStateRepository, + serviceBookingRepository, + testBookingRepository, + appointmentRepository, + messageSource + ); + + ReflectionTestUtils.setField(chatbotService, "restTemplate", restTemplate); + ReflectionTestUtils.setField(chatbotService, "accessToken", "fakeToken"); + ReflectionTestUtils.setField(chatbotService, "timeoutSeconds", 2); // 2 seconds timeout for quick testing + } + + @Test + void testUserInactiveForTwoSeconds_SessionExpires() throws InterruptedException { + String from = "1234567890"; + String phoneId = "987654321"; + UserState userState = new UserState(from); + userState.setState("MAIN_MENU"); + userState.setLanguagePreference("en"); + + when(userStateRepository.findById(from)).thenReturn(Optional.of(userState)); + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class))) + .thenReturn(ResponseEntity.ok("SUCCESS")); + + // Process a message to schedule the timer + chatbotService.processMessage(from, "Some command", phoneId); + + // Verify scheduled task was registered + verify(userStateRepository, atLeastOnce()).save(any(UserState.class)); + assertNotNull(userState.getLastInteractionTime()); + assertEquals("987654321", userState.getLastPhoneNumberId()); + + // Wait for the timeout of 2 seconds (wait 3 seconds to be safe) + Thread.sleep(3000); + + // Verify user state transitions to EXPIRED + ArgumentCaptor userStateCaptor = ArgumentCaptor.forClass(UserState.class); + verify(userStateRepository, atLeast(2)).save(userStateCaptor.capture()); + + UserState finalState = userStateCaptor.getAllValues().get(userStateCaptor.getAllValues().size() - 1); + assertEquals("EXPIRED", finalState.getState()); + assertNull(finalState.getCategory()); + assertNull(finalState.getSubCategory()); + + // Verify fallback message was sent in English + ArgumentCaptor entityCaptor = ArgumentCaptor.forClass(HttpEntity.class); + verify(restTemplate, atLeastOnce()).postForEntity(anyString(), entityCaptor.capture(), eq(String.class)); + + boolean foundFallback = false; + for (HttpEntity entity : entityCaptor.getAllValues()) { + String body = entity.getBody().toString(); + if (body.contains("It looks like your previous conversation has ended") && body.contains("Hi or Hello")) { + foundFallback = true; + break; + } + } + assertTrue(foundFallback, "Expected English fallback message to be sent"); + } + + @Test + void testUserSendsMessageBeforeTimeout_TimerResets() throws InterruptedException { + String from = "1234567890"; + String phoneId = "987654321"; + UserState userState = new UserState(from); + userState.setState("MAIN_MENU"); + userState.setLanguagePreference("en"); + + when(userStateRepository.findById(from)).thenReturn(Optional.of(userState)); + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class))) + .thenReturn(ResponseEntity.ok("SUCCESS")); + + // Initial message schedules timer + chatbotService.processMessage(from, "First message", phoneId); + long initialTime = userState.getLastInteractionTime(); + + // Wait 1 second (less than 2s timeout) and send another message + Thread.sleep(1000); + chatbotService.processMessage(from, "Second message", phoneId); + long resetTime = userState.getLastInteractionTime(); + + assertTrue(resetTime > initialTime, "Last interaction time should have increased"); + + // Wait 1.5 seconds (total 2.5s since first message, but only 1.5s since second message) + Thread.sleep(1500); + + // Verify state is NOT EXPIRED because the 2-second timer has not yet run out from the second interaction + assertNotEquals("EXPIRED", userState.getState()); + + // Now wait another 1.5 seconds (timer should now fire for the second interaction) + Thread.sleep(1500); + assertEquals("EXPIRED", userState.getState()); + } + + @Test + void testExpiredSessionWithGreeting_StartsNewConversation() { + String from = "1234567890"; + String phoneId = "987654321"; + + UserState userState = new UserState(from); + userState.setState("EXPIRED"); + userState.setLanguagePreference("en"); + + when(userStateRepository.findById(from)).thenReturn(Optional.of(userState)); + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class))) + .thenReturn(ResponseEntity.ok("SUCCESS")); + + // Sending "Hi" to start new conversation + chatbotService.processMessage(from, "Hi", phoneId); + + // Since it's a greeting, reset is triggered and state becomes LANGUAGE_SELECTION, + // which then triggers interactive language selection message. + assertEquals("LANGUAGE_SELECTION", userState.getState()); + } + + @Test + void testExpiredSessionWithNonGreeting_SendsReminderMessage() { + String from = "1234567890"; + String phoneId = "987654321"; + + UserState userState = new UserState(from); + userState.setState("EXPIRED"); + userState.setLanguagePreference("en"); + + when(userStateRepository.findById(from)).thenReturn(Optional.of(userState)); + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class))) + .thenReturn(ResponseEntity.ok("SUCCESS")); + + // Sending a non-greeting + chatbotService.processMessage(from, "Book Appointment", phoneId); + + // The state remains EXPIRED + assertEquals("EXPIRED", userState.getState()); + + // Verify reminder message was sent + ArgumentCaptor entityCaptor = ArgumentCaptor.forClass(HttpEntity.class); + verify(restTemplate, times(1)).postForEntity(anyString(), entityCaptor.capture(), eq(String.class)); + String body = entityCaptor.getValue().getBody().toString(); + assertTrue(body.contains("To start a new conversation, please type Hi or Hello"), + "Should remind the user to start a new conversation"); + } + + @Test + void testTamilFallbackMessage() throws InterruptedException { + String from = "1234567890"; + String phoneId = "987654321"; + UserState userState = new UserState(from); + userState.setState("MAIN_MENU"); + userState.setLanguagePreference("ta"); // Tamil preference + + when(userStateRepository.findById(from)).thenReturn(Optional.of(userState)); + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class))) + .thenReturn(ResponseEntity.ok("SUCCESS")); + + // Initial interaction + chatbotService.processMessage(from, "விருப்பம்", phoneId); + + // Wait for the timeout + Thread.sleep(3000); + + assertEquals("EXPIRED", userState.getState()); + + // Verify fallback message sent in Tamil + ArgumentCaptor entityCaptor = ArgumentCaptor.forClass(HttpEntity.class); + verify(restTemplate, atLeastOnce()).postForEntity(anyString(), entityCaptor.capture(), eq(String.class)); + + boolean foundTamilFallback = false; + for (HttpEntity entity : entityCaptor.getAllValues()) { + String body = entity.getBody().toString(); + if (body.contains("உரையாடல் முடிவடைந்துவிட்டது") && body.contains("Hi அல்லது Hello")) { + foundTamilFallback = true; + break; + } + } + assertTrue(foundTamilFallback, "Expected Tamil fallback message to be sent"); + } +} diff --git a/src/test/java/com/example/whatsappautomation/NewWorkflowTests.java b/src/test/java/com/example/whatsappautomation/NewWorkflowTests.java new file mode 100644 index 0000000..ff510cf --- /dev/null +++ b/src/test/java/com/example/whatsappautomation/NewWorkflowTests.java @@ -0,0 +1,353 @@ +package com.example.whatsappautomation; + +import com.example.whatsappautomation.entity.Appointment; +import com.example.whatsappautomation.entity.ServiceBooking; +import com.example.whatsappautomation.entity.TestBooking; +import com.example.whatsappautomation.entity.UserState; +import com.example.whatsappautomation.repository.AppointmentRepository; +import com.example.whatsappautomation.repository.ServiceBookingRepository; +import com.example.whatsappautomation.repository.TestBookingRepository; +import com.example.whatsappautomation.repository.UserStateRepository; +import com.example.whatsappautomation.service.ChatbotService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.context.MessageSource; +import org.springframework.http.HttpEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.RestTemplate; + +import java.util.Locale; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +class NewWorkflowTests { + + private UserStateRepository userStateRepository; + private ServiceBookingRepository serviceBookingRepository; + private TestBookingRepository testBookingRepository; + private AppointmentRepository appointmentRepository; + private MessageSource messageSource; + private RestTemplate restTemplate; + private ChatbotService chatbotService; + + @BeforeEach + void setUp() { + userStateRepository = mock(UserStateRepository.class); + serviceBookingRepository = mock(ServiceBookingRepository.class); + testBookingRepository = mock(TestBookingRepository.class); + appointmentRepository = mock(AppointmentRepository.class); + messageSource = mock(MessageSource.class); + restTemplate = mock(RestTemplate.class); + + chatbotService = new ChatbotService( + userStateRepository, + serviceBookingRepository, + testBookingRepository, + appointmentRepository, + messageSource + ); + + ReflectionTestUtils.setField(chatbotService, "restTemplate", restTemplate); + ReflectionTestUtils.setField(chatbotService, "accessToken", "fakeToken"); + ReflectionTestUtils.setField(chatbotService, "newWorkflowEnabled", true); + ReflectionTestUtils.setField(chatbotService, "timeoutSeconds", 120); + + // Standard stubbing for messageSource to return keys/formatted messages + when(messageSource.getMessage(anyString(), any(), any(Locale.class))) + .thenAnswer(invocation -> { + String key = invocation.getArgument(0); + Object[] args = invocation.getArgument(1); + if (args != null && args.length > 0) { + StringBuilder sb = new StringBuilder(key); + for (Object arg : args) { + sb.append(" [").append(arg).append("]"); + } + return sb.toString(); + } + return key; + }); + + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(String.class))) + .thenReturn(ResponseEntity.ok("SUCCESS")); + } + + @Test + void testFeatureFlagDisabled_FallsBackToOldWorkflow() { + ReflectionTestUtils.setField(chatbotService, "newWorkflowEnabled", false); + + String from = "1234567890"; + UserState userState = new UserState(from); + userState.setState("LANGUAGE_SELECTION"); + when(userStateRepository.findById(from)).thenReturn(Optional.of(userState)); + + chatbotService.processMessage(from, "LANG_EN", "987654321"); + + // When old workflow runs LANG_EN, it should route to the old state machine. + // In the old workflow, LANG_EN transitions to MAIN_MENU. + assertEquals("MAIN_MENU", userState.getState()); + assertEquals("en", userState.getLanguagePreference()); + } + + @Test + void testLanguageSelection_English_Tamil_Hindi() { + String from = "1234567890"; + + // English + UserState userState1 = new UserState(from); + userState1.setState("LANGUAGE_SELECTION"); + when(userStateRepository.findById(from)).thenReturn(Optional.of(userState1)); + chatbotService.processMessage(from, "LANG_EN", "987654321"); + assertEquals("MAIN_MENU", userState1.getState()); + assertEquals("en", userState1.getLanguagePreference()); + + // Tamil + UserState userState2 = new UserState(from); + userState2.setState("LANGUAGE_SELECTION"); + when(userStateRepository.findById(from)).thenReturn(Optional.of(userState2)); + chatbotService.processMessage(from, "LANG_TA", "987654321"); + assertEquals("MAIN_MENU", userState2.getState()); + assertEquals("ta", userState2.getLanguagePreference()); + + // Hindi + UserState userState3 = new UserState(from); + userState3.setState("LANGUAGE_SELECTION"); + when(userStateRepository.findById(from)).thenReturn(Optional.of(userState3)); + chatbotService.processMessage(from, "LANG_HI", "987654321"); + assertEquals("MAIN_MENU", userState3.getState()); + assertEquals("hi", userState3.getLanguagePreference()); + } + + @Test + void testEmergencyFlow() { + String from = "1234567890"; + UserState userState = new UserState(from); + userState.setState("MAIN_MENU"); + userState.setLanguagePreference("en"); + when(userStateRepository.findById(from)).thenReturn(Optional.of(userState)); + + // Go to Emergency + chatbotService.processMessage(from, "NEW_MENU_EMERGENCY", "987654321"); + assertEquals("NEW_EMERGENCY", userState.getState()); + + // Select Main Menu + chatbotService.processMessage(from, "NEW_EMERGENCY_MAIN_MENU", "987654321"); + assertEquals("MAIN_MENU", userState.getState()); + + // Go back to Emergency + chatbotService.processMessage(from, "1", "987654321"); // Option 1 + assertEquals("NEW_EMERGENCY", userState.getState()); + + // Exit + chatbotService.processMessage(from, "NEW_EMERGENCY_EXIT", "987654321"); + assertEquals("EXPIRED", userState.getState()); + } + + @Test + void testAssistanceFlow() { + String from = "1234567890"; + UserState userState = new UserState(from); + userState.setState("MAIN_MENU"); + userState.setLanguagePreference("en"); + when(userStateRepository.findById(from)).thenReturn(Optional.of(userState)); + + // Go to Assistance + chatbotService.processMessage(from, "NEW_MENU_ASSISTANCE", "987654321"); + assertEquals("NEW_ASSISTANCE", userState.getState()); + + // Select Main Menu + chatbotService.processMessage(from, "NEW_ASSISTANCE_MAIN_MENU", "987654321"); + assertEquals("MAIN_MENU", userState.getState()); + } + + @Test + void testJobEnquiryFlow() { + String from = "1234567890"; + UserState userState = new UserState(from); + userState.setState("MAIN_MENU"); + userState.setLanguagePreference("en"); + when(userStateRepository.findById(from)).thenReturn(Optional.of(userState)); + + // Start Job Enquiry + chatbotService.processMessage(from, "NEW_MENU_JOB", "987654321"); + assertEquals("NEW_JOB_PHONE", userState.getState()); + + // Enter invalid phone + chatbotService.processMessage(from, "abc", "987654321"); + assertEquals("NEW_JOB_PHONE", userState.getState()); // Should stay + + // Enter valid phone + chatbotService.processMessage(from, "9876543210", "987654321"); + assertEquals("NEW_JOB_EMAIL", userState.getState()); + assertEquals("9876543210", userState.getSubCategory()); + + // Enter invalid email + chatbotService.processMessage(from, "invalid-email", "987654321"); + assertEquals("NEW_JOB_EMAIL", userState.getState()); // Should stay + + // Enter valid email + chatbotService.processMessage(from, "hr@rmchospital.com", "987654321"); + assertEquals("NEW_JOB_CONFIRM", userState.getState()); + assertEquals("hr@rmchospital.com", userState.getAddress()); + } + + @Test + void testDoctorAppointmentFlow() { + String from = "1234567890"; + UserState userState = new UserState(from); + userState.setState("MAIN_MENU"); + userState.setLanguagePreference("en"); + when(userStateRepository.findById(from)).thenReturn(Optional.of(userState)); + + // Start Doc Appointment + chatbotService.processMessage(from, "NEW_MENU_DOCTOR", "987654321"); + assertEquals("NEW_DOC_SPECIALITY", userState.getState()); + + // Select Cardiology + chatbotService.processMessage(from, "DEPT_CARDIO", "987654321"); + assertEquals("NEW_DOC_DOCTOR", userState.getState()); + assertEquals("Cardiology", userState.getCategory()); + + // Select Doctor Priya + chatbotService.processMessage(from, "DOC_PRIYA", "987654321"); + assertEquals("NEW_DOC_DATE", userState.getState()); + assertEquals("Dr. Priya Sharma", userState.getSelectedItem()); + + // Enter invalid date format + chatbotService.processMessage(from, "2026/08/17", "987654321"); + assertEquals("NEW_DOC_DATE", userState.getState()); // Should stay + + // Enter valid date + chatbotService.processMessage(from, "17/08/2026", "987654321"); + assertEquals("NEW_DOC_TIME", userState.getState()); + assertEquals("17/08/2026", userState.getBookingDate()); + + // Select Time slot 10 AM + chatbotService.processMessage(from, "TIME_10AM", "987654321"); + assertEquals("NEW_DOC_CONFIRM", userState.getState()); + assertEquals("10:00 AM", userState.getBookingTime()); + + // Confirm booking + chatbotService.processMessage(from, "APT_CONFIRM", "987654321"); + + // Verify appointment saved in database + ArgumentCaptor appCaptor = ArgumentCaptor.forClass(Appointment.class); + verify(appointmentRepository, times(1)).save(appCaptor.capture()); + Appointment saved = appCaptor.getValue(); + assertEquals("Cardiology", saved.getDepartment()); + assertEquals("Dr. Priya Sharma", saved.getDoctor()); + assertEquals("17/08/2026", saved.getAppointmentDate()); + assertEquals("10:00 AM", saved.getAppointmentTime()); + assertEquals("Confirmed", saved.getStatus()); + } + + @Test + void testHealthcarePackagesFlow_WithDiscount() { + String from = "1234567890"; + UserState userState = new UserState(from); + userState.setState("MAIN_MENU"); + userState.setLanguagePreference("en"); + when(userStateRepository.findById(from)).thenReturn(Optional.of(userState)); + + // Start Healthcare Package + chatbotService.processMessage(from, "NEW_MENU_PACKAGES", "987654321"); + assertEquals("NEW_PKG_SELECT", userState.getState()); + + // Select Advanced Cardiac Package (₹3000) + chatbotService.processMessage(from, "PKG_CARDIO", "987654321"); + assertEquals("NEW_PKG_COUPON", userState.getState()); + assertEquals("Advanced Cardiac Package", userState.getSelectedItem()); + assertEquals("₹3,000", userState.getPrice()); + + // Enter valid coupon code RMC10 (10% discount) + chatbotService.processMessage(from, "RMC10", "987654321"); + assertEquals("NEW_PKG_DATE", userState.getState()); + assertEquals("₹2,700", userState.getPrice()); // 3000 - 10% = 2700 + + // Enter Date + chatbotService.processMessage(from, "18/08/2026", "987654321"); + assertEquals("NEW_PKG_TIME", userState.getState()); + + // Select time slot + chatbotService.processMessage(from, "PKG_TIME_11AM", "987654321"); + assertEquals("NEW_PKG_CONFIRM", userState.getState()); + assertEquals("11:00 AM", userState.getBookingTime()); + + // Confirm + chatbotService.processMessage(from, "PKG_CONFIRM", "987654321"); + + // Verify package saved in database + ArgumentCaptor bookingCaptor = ArgumentCaptor.forClass(ServiceBooking.class); + verify(serviceBookingRepository, times(1)).save(bookingCaptor.capture()); + ServiceBooking saved = bookingCaptor.getValue(); + assertEquals("Advanced Cardiac Package", saved.getServiceName()); + assertEquals("₹2,700", saved.getPrice()); + assertEquals("18/08/2026", saved.getBookingDate()); + assertEquals("11:00 AM", saved.getBookingTime()); + assertEquals("CONFIRMED", saved.getStatus()); + } + + @Test + void testBloodLabInvestigationFlow() { + String from = "1234567890"; + UserState userState = new UserState(from); + userState.setState("MAIN_MENU"); + userState.setLanguagePreference("en"); + when(userStateRepository.findById(from)).thenReturn(Optional.of(userState)); + + // Start Investigation + chatbotService.processMessage(from, "NEW_MENU_INVESTIGATION", "987654321"); + assertEquals("NEW_INV_TYPE", userState.getState()); + + // Select Blood + chatbotService.processMessage(from, "INV_BLOOD", "987654321"); + assertEquals("NEW_BLOOD_SELECT", userState.getState()); + + // Select Complete Blood Count (CBC) + chatbotService.processMessage(from, "BLOOD_CBC", "987654321"); + assertEquals("NEW_BLOOD_CONFIRM_TEST", userState.getState()); + + // Confirm selection + chatbotService.processMessage(from, "BLOOD_CONFIRM_YES", "987654321"); + assertEquals("NEW_BLOOD_DATE", userState.getState()); + + // Enter Date + chatbotService.processMessage(from, "19/08/2026", "987654321"); + assertEquals("NEW_BLOOD_TIME", userState.getState()); + + // Select Time + chatbotService.processMessage(from, "SLOT_7AM", "987654321"); + assertEquals("NEW_BLOOD_CONFIRM", userState.getState()); + + // Confirm Booking + chatbotService.processMessage(from, "BLOOD_FINAL_CONFIRM", "987654321"); + + // Verify Lab Booking saved in database + ArgumentCaptor testCaptor = ArgumentCaptor.forClass(TestBooking.class); + verify(testBookingRepository, times(1)).save(testCaptor.capture()); + TestBooking saved = testCaptor.getValue(); + assertEquals("Complete Blood Count (CBC)", saved.getPackageName()); + assertEquals("₹300", saved.getPrice()); + assertEquals("19/08/2026", saved.getBookingDate()); + assertEquals("7 AM - 9 AM", saved.getBookingTime()); + assertEquals("CONFIRMED", saved.getStatus()); + } + + @Test + void testNavigationCommands_Back() { + String from = "1234567890"; + UserState userState = new UserState(from); + userState.setState("NEW_DOC_DOCTOR"); + userState.setCategory("Cardiology"); + userState.setLanguagePreference("en"); + when(userStateRepository.findById(from)).thenReturn(Optional.of(userState)); + + // Send 'Back' command to go back to Speciality + chatbotService.processMessage(from, "Back", "987654321"); + assertEquals("NEW_DOC_SPECIALITY", userState.getState()); + } +} diff --git a/src/test/java/com/example/whatsappautomation/WhatsAppAutomationApplicationTests.java b/src/test/java/com/example/whatsappautomation/WhatsAppAutomationApplicationTests.java new file mode 100644 index 0000000..db1d25a --- /dev/null +++ b/src/test/java/com/example/whatsappautomation/WhatsAppAutomationApplicationTests.java @@ -0,0 +1,12 @@ +package com.example.whatsappautomation; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class WhatsAppAutomationApplicationTests { + + @Test + void contextLoads() { + } +} diff --git a/test_restart.py b/test_restart.py new file mode 100644 index 0000000..3c7a664 --- /dev/null +++ b/test_restart.py @@ -0,0 +1,49 @@ +import requests + +payload = { + "object": "whatsapp_business_account", + "entry": [ + { + "id": "1068191009070265", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "15551914329", + "phone_number_id": "1236658909534007" + }, + "contacts": [ + { + "profile": { + "name": "Dev" + }, + "wa_id": "919994834737", + "user_id": "IN.1458184416042595" + } + ], + "messages": [ + { + "from": "919994834737", + "from_user_id": "IN.1458184416042595", + "id": "wamid.HBgMOTE2Mzc5NzgzOTExFQIAEhgWM0VCMDU3MDk2N0Q2ODk0MzE2N0U4NwA=", + "timestamp": "1786945902", + "interactive": { + "type": "button_reply", + "button_reply": { + "id": "MENU_SERVICES", + "title": "My Services" + } + }, + "type": "interactive" + } + ] + }, + "field": "messages" + } + ] + } + ] +} + +requests.post('http://localhost:8085/webhook', json=payload)