From 6e5273548404b580974cf85e921bcb31eb0d977f Mon Sep 17 00:00:00 2001 From: RIT Services Date: Thu, 20 Aug 2026 06:43:00 +0000 Subject: [PATCH] Fees and structures added --- APIforIMS.md | 5 + db/whatsappbot.mv.db | Bin 40960 -> 40960 bytes db/whatsappbot.trace.db | 336 +++++++++++++++ .../controller/WebhookController.java | 19 +- .../whatsapp/handler/StudentMenuHandler.java | 116 ++++-- .../rit/whatsapp/service/ImsApiService.java | 66 ++- .../rit/whatsapp/service/SessionService.java | 22 + .../rit/whatsapp/util/MessageFormatter.java | 387 +++++++++++++++++- src/main/resources/application.properties | 2 +- 9 files changed, 909 insertions(+), 44 deletions(-) diff --git a/APIforIMS.md b/APIforIMS.md index b732d4b..780e66d 100644 --- a/APIforIMS.md +++ b/APIforIMS.md @@ -1215,6 +1215,11 @@ Cache-Control: no-store | GET | `/api/student/results?semester=N` | Yes | | POST | `/api/student/results` | Yes | | GET | `/api/student/fees` | Yes | +| GET | `/api/student/exam-fee` | Yes | +| GET | `/api/student/exam-fee/receipt?history_id=X` | Yes | +| GET | `/api/student/fees/history` | Yes | +| GET | `/api/student/fees/download-receipt?history_id=X` | Yes | +| GET | `/api/student/fees/download-consolidated-receipt?id=X` | Yes | --- diff --git a/db/whatsappbot.mv.db b/db/whatsappbot.mv.db index a123c36473a1574963ab969c43f8e90267d0720a..d10c6e6d9861140099008396e7020691d73891be 100644 GIT binary patch literal 40960 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 XAm#rl|IYb;kn{h50RMMSh5!E_ug87X literal 40960 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 I(?gy4KZWURUjP6A diff --git a/db/whatsappbot.trace.db b/db/whatsappbot.trace.db index 281e5a4..7f115ba 100644 --- a/db/whatsappbot.trace.db +++ b/db/whatsappbot.trace.db @@ -1279,3 +1279,339 @@ Caused by: org.h2.mvstore.MVStoreException: The file is locked: /opt/projects/RI at org.h2.mvstore.SingleFileStore.open(SingleFileStore.java:81) at org.h2.mvstore.MVStore.(MVStore.java:291) ... 151 more +2026-08-20 05:07:04.230651Z database: flush +org.h2.message.DbException: General error: "org.h2.mvstore.MVStoreException: The file is locked: /opt/projects/RITz-whatsapp-bot/db/whatsappbot.mv.db [2.4.240/7]" [50000-240] + at org.h2.message.DbException.get(DbException.java:212) + at org.h2.message.DbException.convert(DbException.java:407) + at org.h2.mvstore.db.Store.lambda$new$0(Store.java:122) + at org.h2.mvstore.MVStore.handleException(MVStore.java:1675) + at org.h2.mvstore.MVStore.panic(MVStore.java:502) + at org.h2.mvstore.MVStore.(MVStore.java:296) + at org.h2.mvstore.MVStore$Builder.open(MVStore.java:2164) + at org.h2.mvstore.db.Store.(Store.java:133) + at org.h2.engine.Database.(Database.java:328) + at org.h2.engine.Engine.openSession(Engine.java:92) + at org.h2.engine.Engine.openSession(Engine.java:222) + at org.h2.engine.Engine.createSession(Engine.java:201) + at org.h2.engine.SessionRemote.connectEmbeddedOrServer(SessionRemote.java:350) + at org.h2.jdbc.JdbcConnection.(JdbcConnection.java:124) + at org.h2.Driver.connect(Driver.java:59) + at com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:144) + at com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:373) + at com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:210) + at com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:488) + at com.zaxxer.hikari.pool.HikariPool.checkFailFast(HikariPool.java:576) + at com.zaxxer.hikari.pool.HikariPool.(HikariPool.java:97) + at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:111) + at org.hibernate.engine.jdbc.connections.internal.DataSourceConnectionProvider.getConnection(DataSourceConnectionProvider.java:149) + at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess.obtainConnection(JdbcEnvironmentInitiator.java:508) + at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcIsolationDelegate.delegateWork(JdbcIsolationDelegate.java:48) + at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.getJdbcEnvironmentUsingJdbcMetadata(JdbcEnvironmentInitiator.java:366) + at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.getJdbcEnvironment(JdbcEnvironmentInitiator.java:143) + at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:120) + at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:80) + at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.initiateService(StandardServiceRegistryImpl.java:133) + at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:260) + at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:235) + at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:212) + at org.hibernate.boot.model.relational.Database.(Database.java:44) + at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.getDatabase(InFlightMetadataCollectorImpl.java:251) + at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.(InFlightMetadataCollectorImpl.java:203) + at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:180) + at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1388) + at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.populateSessionFactoryBuilder(EntityManagerFactoryBuilderImpl.java:1468) + at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1450) + at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:93) + at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:443) + at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:436) + at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:411) + at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:419) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1862) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1811) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:603) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:525) + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:333) + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:371) + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:331) + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:201) + at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:977) + at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:621) + at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:756) + at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:445) + at org.springframework.boot.SpringApplication.run(SpringApplication.java:321) + at org.springframework.boot.test.context.SpringBootContextLoader.lambda$loadContext$2(SpringBootContextLoader.java:155) + at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:58) + at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:46) + at org.springframework.boot.SpringApplication.withHook(SpringApplication.java:1465) + at org.springframework.boot.test.context.SpringBootContextLoader$ContextLoaderHook.run(SpringBootContextLoader.java:600) + at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:155) + at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:114) + at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:247) + at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.lambda$loadContext$0(DefaultCacheAwareContextLoaderDelegate.java:167) + at org.springframework.test.context.cache.DefaultContextCache.put(DefaultContextCache.java:214) + at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:160) + at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:128) + at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:200) + at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:139) + at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:260) + at org.springframework.test.context.junit.jupiter.SpringExtension.postProcessTestInstance(SpringExtension.java:242) + at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$1(ClassBasedTestDescriptor.java:423) + at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.executeAndMaskThrowable(ClassBasedTestDescriptor.java:428) + at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$0(ClassBasedTestDescriptor.java:422) + at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) + at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) + at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179) + at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) + at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1708) + at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) + at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) + at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) + at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) + at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) + at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596) + at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeTestInstancePostProcessors(ClassBasedTestDescriptor.java:422) + at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$instantiateAndPostProcessTestInstance$0(ClassBasedTestDescriptor.java:334) + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:74) + at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.instantiateAndPostProcessTestInstance(ClassBasedTestDescriptor.java:333) + at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$1(ClassBasedTestDescriptor.java:322) + at java.base/java.util.Optional.orElseGet(Optional.java:364) + at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$0(ClassBasedTestDescriptor.java:321) + at org.junit.jupiter.engine.execution.TestInstancesProvider.getTestInstances(TestInstancesProvider.java:27) + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$prepare$0(TestMethodTestDescriptor.java:127) + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:74) + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:126) + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:70) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$prepare$0(NodeTestTask.java:144) + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:74) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.prepare(NodeTestTask.java:144) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:110) + at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:42) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$2(NodeTestTask.java:180) + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:74) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$1(NodeTestTask.java:166) + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:138) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$0(NodeTestTask.java:164) + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:74) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:163) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:116) + at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:42) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$2(NodeTestTask.java:180) + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:74) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$1(NodeTestTask.java:166) + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:138) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$0(NodeTestTask.java:164) + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:74) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:163) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:116) + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:36) + at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:52) + at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:58) + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.executeEngine(EngineExecutionOrchestrator.java:246) + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.failOrExecuteEngine(EngineExecutionOrchestrator.java:218) + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:179) + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108) + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:66) + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:157) + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:65) + at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:125) + at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) + at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:58) + at org.junit.platform.launcher.core.InterceptingLauncher.lambda$execute$2(InterceptingLauncher.java:57) + at org.junit.platform.launcher.core.ClasspathAlignmentCheckingLauncherInterceptor.intercept(ClasspathAlignmentCheckingLauncherInterceptor.java:25) + at org.junit.platform.launcher.core.InterceptingLauncher.execute(InterceptingLauncher.java:56) + at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:58) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) + at java.base/java.lang.reflect.Method.invoke(Method.java:580) + at org.apache.maven.surefire.api.util.ReflectionUtils.invokeMethodWithArray(ReflectionUtils.java:125) + at org.apache.maven.surefire.junitplatform.LauncherAdapter.executeWithCancellationToken(LauncherAdapter.java:68) + at org.apache.maven.surefire.junitplatform.LauncherAdapter.execute(LauncherAdapter.java:54) + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:203) + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:168) + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:136) + at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385) + at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162) + at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507) + at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495) +Caused by: org.h2.jdbc.JdbcSQLNonTransientException: General error: "org.h2.mvstore.MVStoreException: The file is locked: /opt/projects/RITz-whatsapp-bot/db/whatsappbot.mv.db [2.4.240/7]" [50000-240] + at org.h2.message.DbException.getJdbcSQLException(DbException.java:566) + at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) + ... 153 more +Caused by: org.h2.mvstore.MVStoreException: The file is locked: /opt/projects/RITz-whatsapp-bot/db/whatsappbot.mv.db [2.4.240/7] + at org.h2.mvstore.DataUtils.newMVStoreException(DataUtils.java:996) + at org.h2.mvstore.SingleFileStore.lockFileChannel(SingleFileStore.java:143) + at org.h2.mvstore.SingleFileStore.open(SingleFileStore.java:117) + at org.h2.mvstore.SingleFileStore.open(SingleFileStore.java:81) + at org.h2.mvstore.MVStore.(MVStore.java:291) + ... 147 more +2026-08-20 05:07:05.824364Z database: flush +org.h2.message.DbException: General error: "org.h2.mvstore.MVStoreException: The file is locked: /opt/projects/RITz-whatsapp-bot/db/whatsappbot.mv.db [2.4.240/7]" [50000-240] + at org.h2.message.DbException.get(DbException.java:212) + at org.h2.message.DbException.convert(DbException.java:407) + at org.h2.mvstore.db.Store.lambda$new$0(Store.java:122) + at org.h2.mvstore.MVStore.handleException(MVStore.java:1675) + at org.h2.mvstore.MVStore.panic(MVStore.java:502) + at org.h2.mvstore.MVStore.(MVStore.java:296) + at org.h2.mvstore.MVStore$Builder.open(MVStore.java:2164) + at org.h2.mvstore.db.Store.(Store.java:133) + at org.h2.engine.Database.(Database.java:328) + at org.h2.engine.Engine.openSession(Engine.java:92) + at org.h2.engine.Engine.openSession(Engine.java:222) + at org.h2.engine.Engine.createSession(Engine.java:201) + at org.h2.engine.SessionRemote.connectEmbeddedOrServer(SessionRemote.java:350) + at org.h2.jdbc.JdbcConnection.(JdbcConnection.java:124) + at org.h2.Driver.connect(Driver.java:59) + at com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:144) + at com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:373) + at com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:210) + at com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:488) + at com.zaxxer.hikari.pool.HikariPool.checkFailFast(HikariPool.java:576) + at com.zaxxer.hikari.pool.HikariPool.(HikariPool.java:97) + at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:111) + at org.hibernate.engine.jdbc.connections.internal.DataSourceConnectionProvider.getConnection(DataSourceConnectionProvider.java:149) + at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess.obtainConnection(JdbcEnvironmentInitiator.java:508) + at org.hibernate.resource.transaction.backend.jdbc.internal.DdlTransactionIsolatorNonJtaImpl.getIsolatedConnection(DdlTransactionIsolatorNonJtaImpl.java:44) + at org.hibernate.resource.transaction.backend.jdbc.internal.DdlTransactionIsolatorNonJtaImpl.getIsolatedConnection(DdlTransactionIsolatorNonJtaImpl.java:37) + at org.hibernate.tool.schema.internal.exec.ImprovedExtractionContextImpl.getJdbcConnection(ImprovedExtractionContextImpl.java:61) + at org.hibernate.tool.schema.extract.spi.ExtractionContext.getQueryResults(ExtractionContext.java:41) + at org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorLegacyImpl.extractMetadata(SequenceInformationExtractorLegacyImpl.java:36) + at org.hibernate.tool.schema.extract.internal.DatabaseInformationImpl.initializeSequences(DatabaseInformationImpl.java:73) + at org.hibernate.tool.schema.extract.internal.DatabaseInformationImpl.(DatabaseInformationImpl.java:63) + at org.hibernate.tool.schema.extract.internal.CachingDatabaseInformationImpl.(CachingDatabaseInformationImpl.java:43) + at org.hibernate.tool.schema.internal.GroupedSchemaMigratorImpl.buildDatabaseInformation(GroupedSchemaMigratorImpl.java:112) + at org.hibernate.tool.schema.internal.AbstractSchemaMigrator.doMigration(AbstractSchemaMigrator.java:83) + at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.performDatabaseAction(SchemaManagementToolCoordinator.java:269) + at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.lambda$process$1(SchemaManagementToolCoordinator.java:101) + at java.base/java.util.HashMap.forEach(HashMap.java:1429) + at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.process(SchemaManagementToolCoordinator.java:100) + at org.hibernate.boot.internal.SessionFactoryObserverForSchemaExport.sessionFactoryCreated(SessionFactoryObserverForSchemaExport.java:35) + at org.hibernate.internal.SessionFactoryObserverChain.sessionFactoryCreated(SessionFactoryObserverChain.java:33) + at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:327) + at org.hibernate.internal.SessionFactoryRegistry.instantiateSessionFactory(SessionFactoryRegistry.java:64) + at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:458) + at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1452) + at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:93) + at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:443) + at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:436) + at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:411) + at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:419) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1862) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1811) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:603) + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:525) + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:333) + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:371) + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:331) + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:201) + at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:977) + at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:621) + at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:756) + at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:445) + at org.springframework.boot.SpringApplication.run(SpringApplication.java:321) + at org.springframework.boot.test.context.SpringBootContextLoader.lambda$loadContext$2(SpringBootContextLoader.java:155) + at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:58) + at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:46) + at org.springframework.boot.SpringApplication.withHook(SpringApplication.java:1465) + at org.springframework.boot.test.context.SpringBootContextLoader$ContextLoaderHook.run(SpringBootContextLoader.java:600) + at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:155) + at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:114) + at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:247) + at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.lambda$loadContext$0(DefaultCacheAwareContextLoaderDelegate.java:167) + at org.springframework.test.context.cache.DefaultContextCache.put(DefaultContextCache.java:214) + at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:160) + at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:128) + at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:200) + at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:139) + at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:260) + at org.springframework.test.context.junit.jupiter.SpringExtension.postProcessTestInstance(SpringExtension.java:242) + at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$1(ClassBasedTestDescriptor.java:423) + at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.executeAndMaskThrowable(ClassBasedTestDescriptor.java:428) + at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$0(ClassBasedTestDescriptor.java:422) + at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) + at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) + at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179) + at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) + at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1708) + at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) + at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) + at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) + at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) + at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) + at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596) + at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeTestInstancePostProcessors(ClassBasedTestDescriptor.java:422) + at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$instantiateAndPostProcessTestInstance$0(ClassBasedTestDescriptor.java:334) + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:74) + at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.instantiateAndPostProcessTestInstance(ClassBasedTestDescriptor.java:333) + at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$1(ClassBasedTestDescriptor.java:322) + at java.base/java.util.Optional.orElseGet(Optional.java:364) + at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$0(ClassBasedTestDescriptor.java:321) + at org.junit.jupiter.engine.execution.TestInstancesProvider.getTestInstances(TestInstancesProvider.java:27) + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$prepare$0(TestMethodTestDescriptor.java:127) + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:74) + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:126) + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:70) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$prepare$0(NodeTestTask.java:144) + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:74) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.prepare(NodeTestTask.java:144) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:110) + at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:42) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$2(NodeTestTask.java:180) + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:74) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$1(NodeTestTask.java:166) + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:138) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$0(NodeTestTask.java:164) + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:74) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:163) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:116) + at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:42) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$2(NodeTestTask.java:180) + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:74) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$1(NodeTestTask.java:166) + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:138) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$0(NodeTestTask.java:164) + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:74) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:163) + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:116) + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:36) + at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:52) + at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:58) + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.executeEngine(EngineExecutionOrchestrator.java:246) + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.failOrExecuteEngine(EngineExecutionOrchestrator.java:218) + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:179) + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108) + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:66) + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:157) + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:65) + at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:125) + at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) + at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:58) + at org.junit.platform.launcher.core.InterceptingLauncher.lambda$execute$2(InterceptingLauncher.java:57) + at org.junit.platform.launcher.core.ClasspathAlignmentCheckingLauncherInterceptor.intercept(ClasspathAlignmentCheckingLauncherInterceptor.java:25) + at org.junit.platform.launcher.core.InterceptingLauncher.execute(InterceptingLauncher.java:56) + at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:58) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) + at java.base/java.lang.reflect.Method.invoke(Method.java:580) + at org.apache.maven.surefire.api.util.ReflectionUtils.invokeMethodWithArray(ReflectionUtils.java:125) + at org.apache.maven.surefire.junitplatform.LauncherAdapter.executeWithCancellationToken(LauncherAdapter.java:68) + at org.apache.maven.surefire.junitplatform.LauncherAdapter.execute(LauncherAdapter.java:54) + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:203) + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:168) + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:136) + at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385) + at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162) + at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507) + at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495) +Caused by: org.h2.jdbc.JdbcSQLNonTransientException: General error: "org.h2.mvstore.MVStoreException: The file is locked: /opt/projects/RITz-whatsapp-bot/db/whatsappbot.mv.db [2.4.240/7]" [50000-240] + at org.h2.message.DbException.getJdbcSQLException(DbException.java:566) + at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) + ... 157 more +Caused by: org.h2.mvstore.MVStoreException: The file is locked: /opt/projects/RITz-whatsapp-bot/db/whatsappbot.mv.db [2.4.240/7] + at org.h2.mvstore.DataUtils.newMVStoreException(DataUtils.java:996) + at org.h2.mvstore.SingleFileStore.lockFileChannel(SingleFileStore.java:143) + at org.h2.mvstore.SingleFileStore.open(SingleFileStore.java:117) + at org.h2.mvstore.SingleFileStore.open(SingleFileStore.java:81) + at org.h2.mvstore.MVStore.(MVStore.java:291) + ... 151 more diff --git a/src/main/java/com/rit/whatsapp/controller/WebhookController.java b/src/main/java/com/rit/whatsapp/controller/WebhookController.java index 6d85dce..6e09149 100644 --- a/src/main/java/com/rit/whatsapp/controller/WebhookController.java +++ b/src/main/java/com/rit/whatsapp/controller/WebhookController.java @@ -75,6 +75,13 @@ public class WebhookController { String messageType = messageNode.get("type").asText(); UserState currentState = sessionService.getUserState(fromPhoneNumber); + + boolean isInMemory = sessionService.isSessionCached(fromPhoneNumber); + boolean hasDbRecord = sessionService.hasSavedCredentials(fromPhoneNumber); + if (!isInMemory && hasDbRecord) { + messageService.sendMessage(fromPhoneNumber, "Authenticating User with IMS... ⏳"); + } + boolean isAuthenticated = sessionService.isAuthenticated(fromPhoneNumber); // Global reset command @@ -166,7 +173,7 @@ public class WebhookController { messageService.sendMessage(phoneNumber, "This menu selection is no longer active."); studentMenuHandler.sendMainMenu(phoneNumber); } - } else if (selectedListId.startsWith("receipt_")) { + } else if (selectedListId.startsWith("receipt_") || selectedListId.startsWith("exam_") || selectedListId.startsWith("acad_") || selectedListId.startsWith("cons_")) { // They clicked a receipt download option if (currentState == UserState.AWAITING_RECEIPT_SELECTION) { studentMenuHandler.handleReceiptSelection(phoneNumber, selectedListId); @@ -191,6 +198,16 @@ public class WebhookController { return; } + if ("student_exam_fees".equals(selectedButtonId)) { + studentMenuHandler.handleExamFees(phoneNumber); + return; + } + + if ("student_payment_history".equals(selectedButtonId)) { + studentMenuHandler.handlePaymentHistory(phoneNumber); + return; + } + if (currentState == UserState.AWAITING_GRIEVANCE_CATEGORY) { grievanceHandler.handleCategorySelection(phoneNumber, selectedButtonId); } else { diff --git a/src/main/java/com/rit/whatsapp/handler/StudentMenuHandler.java b/src/main/java/com/rit/whatsapp/handler/StudentMenuHandler.java index 1a63e24..d1f1b04 100644 --- a/src/main/java/com/rit/whatsapp/handler/StudentMenuHandler.java +++ b/src/main/java/com/rit/whatsapp/handler/StudentMenuHandler.java @@ -97,30 +97,29 @@ public class StudentMenuHandler { sendSemesterSelection(phoneNumber, token); break; - case "student_fee_details": + case "student_fee_details": { String rawFees = imsApiService.getFees(token); - String rawExamFees = imsApiService.getExamFees(token); - String bodyText = MessageFormatter.formatFeesAndExamFees(rawFees, rawExamFees); + String bodyText = MessageFormatter.formatAcademicFeesOnly(rawFees); - List> receiptRows = MessageFormatter.extractReceiptRows(rawExamFees); + List> feeButtons = new ArrayList<>(); + Map examBtn = new HashMap<>(); + examBtn.put("id", "student_exam_fees"); + examBtn.put("title", "Exam Fees"); + feeButtons.add(examBtn); - if (!receiptRows.isEmpty()) { - // Max 10 items in a list. Limit to 10 if necessary. - if (receiptRows.size() > 10) receiptRows = receiptRows.subList(0, 10); - - sessionService.setUserState(phoneNumber, UserState.AWAITING_RECEIPT_SELECTION); - - // Meta API interactive list body limit is 1024 characters - if (bodyText.length() > 1024) bodyText = bodyText.substring(0, 1021) + "..."; - - Map listMessage = WhatsAppMessageBuilder.buildListMessage( - "Receipts", bodyText, "Download Receipt", receiptRows - ); - messageService.sendInteractiveMessage(phoneNumber, listMessage); - } else { - sendResponseWithMenu(phoneNumber, bodyText); + Map historyBtn = new HashMap<>(); + historyBtn.put("id", "student_payment_history"); + historyBtn.put("title", "Payment History"); + feeButtons.add(historyBtn); + + if (bodyText.length() > 1020) { + bodyText = bodyText.substring(0, 1017) + "..."; } + + Map feeButtonMessage = WhatsAppMessageBuilder.buildButtonMessage(bodyText, feeButtons); + messageService.sendInteractiveMessage(phoneNumber, feeButtonMessage); break; + } case "student_raise_grievance": grievanceHandler.startGrievanceFlow(phoneNumber); @@ -209,15 +208,31 @@ public class StudentMenuHandler { } try { - String historyId = selectedOptionId.replace("receipt_", ""); - byte[] pdfBytes = imsApiService.getExamFeeReceiptPdf(token, historyId); + byte[] pdfBytes = null; + String fileName = "Receipt.pdf"; + String docMessage = "Here is your Fee Receipt!"; + + if (selectedOptionId.startsWith("exam_")) { + String longId = sessionService.getReceiptIdMapping(phoneNumber, selectedOptionId); + pdfBytes = imsApiService.getExamFeeReceiptPdf(token, longId); + fileName = "Exam_Receipt.pdf"; + docMessage = "Here is your Exam Fee Receipt!"; + } else if (selectedOptionId.startsWith("acad_")) { + String longId = sessionService.getReceiptIdMapping(phoneNumber, selectedOptionId); + pdfBytes = imsApiService.getAcademicFeeReceiptPdf(token, longId); + fileName = "Academic_Receipt.pdf"; + docMessage = "Here is your Academic Fee Receipt!"; + } else if (selectedOptionId.startsWith("cons_")) { + String longId = sessionService.getReceiptIdMapping(phoneNumber, selectedOptionId); + pdfBytes = imsApiService.getConsolidatedAcademicFeeReceiptPdf(token, longId); + fileName = "Consolidated_Receipt.pdf"; + docMessage = "Here is your Consolidated Academic Fee Receipt!"; + } if (pdfBytes != null) { - String fileName = "Receipt_" + historyId + ".pdf"; String mediaId = messageService.uploadMedia(pdfBytes, fileName, "application/pdf"); - if (mediaId != null) { - messageService.sendDocumentMessage(phoneNumber, mediaId, "Here is your Exam Fee Receipt!", fileName); + messageService.sendDocumentMessage(phoneNumber, mediaId, docMessage, fileName); sendResponseWithMenu(phoneNumber, "Select an option from the menu below."); } else { sendResponseWithMenu(phoneNumber, "Failed to upload the receipt document to WhatsApp."); @@ -249,6 +264,59 @@ public class StudentMenuHandler { sendResponseWithMenu(phoneNumber, "Select an option from the menu below."); } + public void handleExamFees(String phoneNumber) { + String token = sessionService.getImsToken(phoneNumber); + if (token == null) { + messageService.sendMessage(phoneNumber, "Your session has expired. Please type 'Reset' to login again."); + return; + } + + String rawExamFees = imsApiService.getExamFees(token); + String formattedExamFees = MessageFormatter.formatExamFees(rawExamFees); + sendResponseWithMenu(phoneNumber, formattedExamFees); + } + + public void handlePaymentHistory(String phoneNumber) { + String token = sessionService.getImsToken(phoneNumber); + if (token == null) { + messageService.sendMessage(phoneNumber, "Your session has expired. Please type 'Reset' to login again."); + return; + } + + String rawExamFees = imsApiService.getExamFees(token); + String rawFeesHistory = imsApiService.getAcademicFeesHistory(token); + + String formattedHistory = MessageFormatter.formatCombinedPaymentHistory(rawExamFees, rawFeesHistory); + + Map idMappings = new HashMap<>(); + List> examRows = MessageFormatter.extractReceiptRows(rawExamFees, idMappings); + List> acadRows = MessageFormatter.extractAcademicReceiptRows(rawFeesHistory, idMappings); + List> consRows = MessageFormatter.extractConsolidatedReceiptRows(rawFeesHistory, idMappings); + + sessionService.storeReceiptIdMappings(phoneNumber, idMappings); + + List> receiptRows = new ArrayList<>(); + receiptRows.addAll(examRows); + receiptRows.addAll(acadRows); + receiptRows.addAll(consRows); + + if (!receiptRows.isEmpty()) { + if (receiptRows.size() > 10) receiptRows = receiptRows.subList(0, 10); + sessionService.setUserState(phoneNumber, UserState.AWAITING_RECEIPT_SELECTION); + + if (formattedHistory.length() > 1024) { + formattedHistory = formattedHistory.substring(0, 1021) + "..."; + } + + Map listMessage = WhatsAppMessageBuilder.buildListMessage( + "Receipts", formattedHistory, "Download Receipt", receiptRows + ); + messageService.sendInteractiveMessage(phoneNumber, listMessage); + } else { + sendResponseWithMenu(phoneNumber, formattedHistory); + } + } + private void sendResponseWithMenu(String phoneNumber, String bodyText) { // Meta API interactive list body limit is 1024 characters if (bodyText.length() > 1024) { diff --git a/src/main/java/com/rit/whatsapp/service/ImsApiService.java b/src/main/java/com/rit/whatsapp/service/ImsApiService.java index 304d890..7dd9ce9 100644 --- a/src/main/java/com/rit/whatsapp/service/ImsApiService.java +++ b/src/main/java/com/rit/whatsapp/service/ImsApiService.java @@ -134,7 +134,15 @@ public class ImsApiService { } public byte[] getExamFeeReceiptPdf(String token, String historyId) { - String url = BASE_URL + "/api/student/exam-fee/receipt?history_id=" + historyId; + String cleanId = historyId; + if (cleanId.startsWith("?history_id=")) { + cleanId = cleanId.substring(12); + } else if (cleanId.startsWith("?id=")) { + cleanId = cleanId.substring(4); + } else if (cleanId.startsWith("?")) { + cleanId = cleanId.substring(1); + } + String url = BASE_URL + "/api/student/exam-fee/receipt?history_id=" + cleanId; HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(token); @@ -186,4 +194,60 @@ public class ImsApiService { } return "Student"; } + + public String getAcademicFeesHistory(String token) { + return fetchFromIms("/api/student/fees/history", token); + } + + public byte[] getAcademicFeeReceiptPdf(String token, String historyId) { + String cleanId = historyId; + if (cleanId.startsWith("?history_id=")) { + cleanId = cleanId.substring(12); + } else if (cleanId.startsWith("?id=")) { + cleanId = cleanId.substring(4); + } else if (cleanId.startsWith("?")) { + cleanId = cleanId.substring(1); + } + String url = BASE_URL + "/api/student/fees/download-receipt?history_id=" + cleanId; + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(token); + + HttpEntity request = new HttpEntity<>(headers); + + try { + ResponseEntity response = restTemplate.exchange(url, org.springframework.http.HttpMethod.GET, request, byte[].class); + if (response.getStatusCode().is2xxSuccessful()) { + return response.getBody(); + } + } catch (Exception e) { + System.err.println("Failed to download Academic Fee receipt PDF: " + e.getMessage()); + } + return null; + } + + public byte[] getConsolidatedAcademicFeeReceiptPdf(String token, String id) { + String cleanId = id; + if (cleanId.startsWith("?id=")) { + cleanId = cleanId.substring(4); + } else if (cleanId.startsWith("?history_id=")) { + cleanId = cleanId.substring(12); + } else if (cleanId.startsWith("?")) { + cleanId = cleanId.substring(1); + } + String url = BASE_URL + "/api/student/fees/download-consolidated-receipt?id=" + cleanId; + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(token); + + HttpEntity request = new HttpEntity<>(headers); + + try { + ResponseEntity response = restTemplate.exchange(url, org.springframework.http.HttpMethod.GET, request, byte[].class); + if (response.getStatusCode().is2xxSuccessful()) { + return response.getBody(); + } + } catch (Exception e) { + System.err.println("Failed to download Consolidated Academic Fee receipt PDF: " + e.getMessage()); + } + return null; + } } diff --git a/src/main/java/com/rit/whatsapp/service/SessionService.java b/src/main/java/com/rit/whatsapp/service/SessionService.java index 4b37408..4044d0a 100644 --- a/src/main/java/com/rit/whatsapp/service/SessionService.java +++ b/src/main/java/com/rit/whatsapp/service/SessionService.java @@ -42,6 +42,14 @@ public class SessionService { userStates.put(phoneNumber, state); } + public boolean isSessionCached(String phoneNumber) { + return sessionTokens.containsKey(phoneNumber); + } + + public boolean hasSavedCredentials(String phoneNumber) { + return userRepository.existsById(phoneNumber); + } + public void resetState(String phoneNumber) { userStates.remove(phoneNumber); } @@ -104,6 +112,20 @@ public class SessionService { return name; } + private final Map> receiptIdMappings = new ConcurrentHashMap<>(); + + public void storeReceiptIdMappings(String phoneNumber, Map mappings) { + receiptIdMappings.put(phoneNumber, mappings); + } + + public String getReceiptIdMapping(String phoneNumber, String shortId) { + Map mappings = receiptIdMappings.get(phoneNumber); + if (mappings != null) { + return mappings.get(shortId); + } + return null; + } + public void setTempLoginId(String phoneNumber, String tempId) { temporaryLoginIds.put(phoneNumber, tempId); } diff --git a/src/main/java/com/rit/whatsapp/util/MessageFormatter.java b/src/main/java/com/rit/whatsapp/util/MessageFormatter.java index 4e83e2f..8a05ffa 100644 --- a/src/main/java/com/rit/whatsapp/util/MessageFormatter.java +++ b/src/main/java/com/rit/whatsapp/util/MessageFormatter.java @@ -51,10 +51,9 @@ public class MessageFormatter { if (!foundData) { sb.append("Details not available in this format.\n"); - sb.append("\n*DEBUG RAW ATTENDANCE JSON:*\n").append(rawAttendance).append("\n"); } } catch (Exception e) { - sb.append("⚠️ Error formatting attendance data.\nRAW JSON: ").append(rawAttendance).append("\n"); + sb.append("⚠️ Error formatting attendance data.\n"); } } @@ -85,10 +84,9 @@ public class MessageFormatter { if (!foundData) { sb.append("Details not available in this format.\n"); - sb.append("\n*DEBUG RAW LEAVES JSON:*\n").append(rawLeaves).append("\n"); } } catch (Exception e) { - sb.append("⚠️ Error formatting leaves data.\nRAW JSON: ").append(rawLeaves).append("\n"); + sb.append("⚠️ Error formatting leaves data.\n"); } } @@ -482,23 +480,159 @@ public class MessageFormatter { return sb.toString(); } - public static java.util.List> extractReceiptRows(String rawExamFees) { + public static String formatAcademicFeesOnly(String rawFees) { + StringBuilder sb = new StringBuilder(); + sb.append("💵 *Academic Dues / Fees*\n\n"); + if (rawFees == null) { + sb.append("❌ Failed to retrieve academic fee details.\n"); + } else { + try { + JsonNode root = mapper.readTree(rawFees); + boolean foundData = false; + + if (root.has("data")) { + JsonNode data = root.get("data"); + if (data.isArray() && data.size() > 0) { + for (JsonNode fee : data) { + String type = fee.has("type") ? fee.get("type").asText() : + (fee.has("description") ? fee.get("description").asText() : "Fee"); + String amount = fee.has("amount") ? fee.get("amount").asText() : "0"; + String status = fee.has("status") ? fee.get("status").asText() : "N/A"; + + sb.append("- *").append(type).append("*: ₹").append(amount).append(" (").append(status).append(")\n"); + foundData = true; + } + } else if (data.isObject()) { + java.util.Iterator> fields = data.fields(); + while (fields.hasNext()) { + java.util.Map.Entry field = fields.next(); + sb.append("- *").append(field.getKey().replace("_", " ")).append("*: ").append(field.getValue().asText()).append("\n"); + foundData = true; + } + } + } + + if (!foundData) { + sb.append("Details not available in this format.\n"); + } + } catch (Exception e) { + sb.append("⚠️ Error formatting academic fee data.\n"); + } + } + return sb.toString(); + } + + public static String formatExamFees(String rawExamFees) { + StringBuilder sb = new StringBuilder(); + sb.append("📝 *Exam Fees*\n\n"); + if (rawExamFees == null) { + sb.append("❌ Failed to retrieve exam fee details.\n"); + } else { + try { + JsonNode root = mapper.readTree(rawExamFees); + boolean foundData = false; + + if (root.has("data")) { + JsonNode data = root.get("data"); + if (data.isArray() && data.size() > 0) { + for (JsonNode fee : data) { + String status = fee.has("status") ? fee.get("status").asText() : ""; + if (!status.equalsIgnoreCase("Paid") && !status.equalsIgnoreCase("Success")) { + String semester = fee.has("semester") ? fee.get("semester").asText() : "Unknown Sem"; + String amount = fee.has("amount") ? fee.get("amount").asText() : "0"; + String displayStatus = status.isEmpty() ? "Unpaid" : status; + sb.append("- *Sem ").append(semester).append("*: ₹").append(amount).append(" (").append(displayStatus).append(")\n"); + foundData = true; + } + } + } + } + + if (!foundData) { + sb.append("No outstanding exam fees found.\n"); + } + } catch (Exception e) { + sb.append("⚠️ Error formatting exam fee data.\n"); + } + } + return sb.toString(); + } + + public static String formatPaymentHistory(String rawExamFees) { + StringBuilder sb = new StringBuilder(); + sb.append("📜 *Payment History*\n\n"); + if (rawExamFees == null) { + sb.append("❌ Failed to retrieve payment history.\n"); + } else { + try { + JsonNode root = mapper.readTree(rawExamFees); + boolean foundData = false; + + if (root.has("data")) { + JsonNode data = root.get("data"); + if (data.isArray() && data.size() > 0) { + for (JsonNode fee : data) { + String status = fee.has("status") ? fee.get("status").asText() : ""; + if (status.equalsIgnoreCase("Paid") || status.equalsIgnoreCase("Success") || fee.has("history_id")) { + String semester = fee.has("semester") ? fee.get("semester").asText() : "Unknown Sem"; + String amount = fee.has("amount") ? fee.get("amount").asText() : "0"; + String date = fee.has("date") ? " on " + fee.get("date").asText() : ""; + sb.append("- *Sem ").append(semester).append("*: ₹").append(amount).append(" (Paid").append(date).append(")\n"); + foundData = true; + } + } + } + } + + if (!foundData) { + sb.append("No payment history found.\n"); + } + } catch (Exception e) { + sb.append("⚠️ Error formatting payment history.\n"); + } + } + return sb.toString(); + } + + public static java.util.List> extractReceiptRows(String rawExamFees, java.util.Map idMappings) { java.util.List> rows = new java.util.ArrayList<>(); if (rawExamFees == null) return rows; try { JsonNode root = mapper.readTree(rawExamFees); - if (root.has("data") && root.get("data").isArray()) { - for (JsonNode fee : root.get("data")) { - if (fee.has("history_id") || fee.has("id")) { - String id = fee.has("history_id") ? fee.get("history_id").asText() : fee.get("id").asText(); - String amount = fee.has("amount") ? fee.get("amount").asText() : ""; - String date = fee.has("date") ? fee.get("date").asText() : ""; - - java.util.Map row = new java.util.HashMap<>(); - row.put("id", "receipt_" + id); - row.put("title", "Receipt " + id); - row.put("description", "₹" + amount + (date.isEmpty() ? "" : " on " + date)); - rows.add(row); + if (root.has("data")) { + JsonNode data = root.get("data"); + JsonNode historyNode = null; + if (data.isObject() && data.has("history")) { + historyNode = data.get("history"); + } + + if (historyNode != null && historyNode.isObject()) { + java.util.Iterator> fields = historyNode.fields(); + while (fields.hasNext()) { + java.util.Map.Entry field = fields.next(); + JsonNode arrayNode = field.getValue(); + if (arrayNode.isArray()) { + for (JsonNode fee : arrayNode) { + if (fee.has("history_id") || fee.has("id")) { + String id = fee.has("history_id") ? fee.get("history_id").asText() : fee.get("id").asText(); + String amount = fee.has("paid_amt") ? fee.get("paid_amt").asText() : (fee.has("amount") ? fee.get("amount").asText() : ""); + String date = fee.has("paid_date") ? fee.get("paid_date").asText() : (fee.has("date") ? fee.get("date").asText() : ""); + + java.util.Map row = new java.util.HashMap<>(); + String shortId = "exam_" + (rows.size() + 1); + idMappings.put(shortId, id); + row.put("id", shortId); + + String title = "Receipt " + date; + if (title.length() > 24 || date.isEmpty()) { + title = "Exam Receipt #" + (rows.size() + 1); + } + row.put("title", title); + row.put("description", "₹" + amount + (date.isEmpty() ? "" : " on " + date)); + rows.add(row); + } + } + } } } } @@ -506,6 +640,225 @@ public class MessageFormatter { return rows; } + public static java.util.List> extractAcademicReceiptRows(String rawFeesHistory, java.util.Map idMappings) { + java.util.List> rows = new java.util.ArrayList<>(); + if (rawFeesHistory == null) return rows; + try { + JsonNode root = mapper.readTree(rawFeesHistory); + if (root.has("data")) { + JsonNode data = root.get("data"); + JsonNode historyNode = null; + + if (data.isObject()) { + if (data.has("history")) historyNode = data.get("history"); + else if (data.has("payment_history")) historyNode = data.get("payment_history"); + else if (data.has("paymentHistory")) historyNode = data.get("paymentHistory"); + else if (data.has("logs")) historyNode = data.get("logs"); + } + + if (historyNode != null) { + if (historyNode.isObject()) { + java.util.Iterator> fields = historyNode.fields(); + while (fields.hasNext()) { + java.util.Map.Entry field = fields.next(); + JsonNode arrayNode = field.getValue(); + if (arrayNode.isArray()) { + for (JsonNode fee : arrayNode) { + if (fee.has("history_id") || fee.has("id")) { + String id = fee.has("history_id") ? fee.get("history_id").asText() : fee.get("id").asText(); + String amount = fee.has("paid_amt") ? fee.get("paid_amt").asText() : (fee.has("amount") ? fee.get("amount").asText() : ""); + String date = fee.has("paid_date") ? fee.get("paid_date").asText() : (fee.has("date") ? fee.get("date").asText() : ""); + String type = fee.has("type") ? fee.get("type").asText() : + (fee.has("description") ? fee.get("description").asText() : (fee.has("name") ? fee.get("name").asText() : "Academic Fee")); + + java.util.Map row = new java.util.HashMap<>(); + String shortId = "acad_" + (rows.size() + 1); + idMappings.put(shortId, id); + row.put("id", shortId); + + String title = "Receipt " + date; + if (title.length() > 24 || date.isEmpty()) { + title = "Acad Receipt #" + (rows.size() + 1); + } + row.put("title", title); + row.put("description", type + ": ₹" + amount + (date.isEmpty() ? "" : " on " + date)); + rows.add(row); + } + } + } + } + } else if (historyNode.isArray()) { + for (JsonNode fee : historyNode) { + if (fee.has("history_id") || fee.has("id")) { + String id = fee.has("history_id") ? fee.get("history_id").asText() : fee.get("id").asText(); + String amount = fee.has("paid_amt") ? fee.get("paid_amt").asText() : (fee.has("amount") ? fee.get("amount").asText() : ""); + String date = fee.has("paid_date") ? fee.get("paid_date").asText() : (fee.has("date") ? fee.get("date").asText() : ""); + String type = fee.has("type") ? fee.get("type").asText() : + (fee.has("description") ? fee.get("description").asText() : "Academic Fee"); + + java.util.Map row = new java.util.HashMap<>(); + String shortId = "acad_" + (rows.size() + 1); + idMappings.put(shortId, id); + row.put("id", shortId); + + String title = "Receipt " + date; + if (title.length() > 24 || date.isEmpty()) { + title = "Acad Receipt #" + (rows.size() + 1); + } + row.put("title", title); + row.put("description", type + ": ₹" + amount + (date.isEmpty() ? "" : " on " + date)); + rows.add(row); + } + } + } + } + } + } catch (Exception e) {} + return rows; + } + + public static java.util.List> extractConsolidatedReceiptRows(String rawFeesHistory, java.util.Map idMappings) { + java.util.List> rows = new java.util.ArrayList<>(); + if (rawFeesHistory == null) return rows; + try { + JsonNode root = mapper.readTree(rawFeesHistory); + if (root.has("data") && root.get("data").isObject()) { + JsonNode data = root.get("data"); + JsonNode consolidatedNode = null; + + if (data.has("consolidated")) consolidatedNode = data.get("consolidated"); + else if (data.has("consolidated_receipts")) consolidatedNode = data.get("consolidated_receipts"); + else if (data.has("consolidatedReceipts")) consolidatedNode = data.get("consolidatedReceipts"); + + if (consolidatedNode != null && consolidatedNode.isArray()) { + for (JsonNode rec : consolidatedNode) { + if (rec.has("id")) { + String id = rec.get("id").asText(); + String academicYear = rec.has("academic_year") ? rec.get("academic_year").asText() : ""; + String totalFee = rec.has("total_fee") ? rec.get("total_fee").asText() : ""; + String paidFee = rec.has("paid_fee") ? rec.get("paid_fee").asText() : ""; + + java.util.Map row = new java.util.HashMap<>(); + String shortId = "cons_" + (rows.size() + 1); + idMappings.put(shortId, id); + row.put("id", shortId); + + // WhatsApp interactive list row titles must be <= 24 characters + String title = "Consolidated " + academicYear; + if (title.length() > 24 || academicYear.isEmpty()) { + title = "Consolidated #" + (rows.size() + 1); + } + row.put("title", title); + + String description = "Paid: ₹" + paidFee + " / ₹" + totalFee; + if (description.length() > 72) { + description = description.substring(0, 69) + "..."; + } + row.put("description", description); + rows.add(row); + } + } + } + } + } catch (Exception e) {} + return rows; + } + + public static String formatCombinedPaymentHistory(String rawExamFees, String rawFeesHistory) { + StringBuilder sb = new StringBuilder(); + + // --- Parse Exam Fee History --- + sb.append("📝 *Exam Fee Payment History*\n\n"); + boolean foundExam = false; + if (rawExamFees != null) { + try { + JsonNode root = mapper.readTree(rawExamFees); + if (root.has("data")) { + JsonNode data = root.get("data"); + JsonNode historyNode = null; + if (data.isObject() && data.has("history")) { + historyNode = data.get("history"); + } + + if (historyNode != null && historyNode.isObject()) { + java.util.Iterator> fields = historyNode.fields(); + while (fields.hasNext()) { + java.util.Map.Entry field = fields.next(); + JsonNode arrayNode = field.getValue(); + if (arrayNode.isArray()) { + for (JsonNode fee : arrayNode) { + String semester = fee.has("semester") ? fee.get("semester").asText() : "Unknown Sem"; + String amount = fee.has("paid_amt") ? fee.get("paid_amt").asText() : (fee.has("amount") ? fee.get("amount").asText() : "0"); + String date = fee.has("paid_date") ? " on " + fee.get("paid_date").asText() : (fee.has("date") ? " on " + fee.get("date").asText() : ""); + sb.append("- *Sem ").append(semester).append("*: ₹").append(amount).append(" (Paid").append(date).append(")\n"); + foundExam = true; + } + } + } + } + } + } catch (Exception e) {} + } + if (!foundExam) { + sb.append("No exam fee payment history found.\n"); + } + + sb.append("\n"); + + // --- Parse Academic Fee History --- + sb.append("💵 *Academic Fee Payment History*\n\n"); + boolean foundAcad = false; + if (rawFeesHistory != null) { + try { + JsonNode root = mapper.readTree(rawFeesHistory); + if (root.has("data")) { + JsonNode data = root.get("data"); + JsonNode historyNode = null; + if (data.isObject()) { + if (data.has("history")) historyNode = data.get("history"); + else if (data.has("payment_history")) historyNode = data.get("payment_history"); + else if (data.has("paymentHistory")) historyNode = data.get("paymentHistory"); + else if (data.has("logs")) historyNode = data.get("logs"); + } + + if (historyNode != null) { + if (historyNode.isObject()) { + java.util.Iterator> fields = historyNode.fields(); + while (fields.hasNext()) { + java.util.Map.Entry field = fields.next(); + JsonNode arrayNode = field.getValue(); + if (arrayNode.isArray()) { + for (JsonNode fee : arrayNode) { + String amount = fee.has("paid_amt") ? fee.get("paid_amt").asText() : (fee.has("amount") ? fee.get("amount").asText() : ""); + String date = fee.has("paid_date") ? " on " + fee.get("paid_date").asText() : (fee.has("date") ? " on " + fee.get("date").asText() : ""); + String type = fee.has("type") ? fee.get("type").asText() : + (fee.has("description") ? fee.get("description").asText() : (fee.has("name") ? fee.get("name").asText() : "Academic Fee")); + sb.append("- *").append(type).append("*: ₹").append(amount).append(" (Paid").append(date).append(")\n"); + foundAcad = true; + } + } + } + } else if (historyNode.isArray()) { + for (JsonNode fee : historyNode) { + String amount = fee.has("paid_amt") ? fee.get("paid_amt").asText() : (fee.has("amount") ? fee.get("amount").asText() : ""); + String date = fee.has("paid_date") ? " on " + fee.get("paid_date").asText() : (fee.has("date") ? " on " + fee.get("date").asText() : ""); + String type = fee.has("type") ? fee.get("type").asText() : + (fee.has("description") ? fee.get("description").asText() : "Academic Fee"); + sb.append("- *").append(type).append("*: ₹").append(amount).append(" (Paid").append(date).append(")\n"); + foundAcad = true; + } + } + } + } + } catch (Exception e) {} + } + if (!foundAcad) { + sb.append("No academic fee payment history found.\n"); + } + + return sb.toString(); + } + private static String getNumberEmoji(int num) { switch (num) { case 1: return "1️⃣"; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 700cd92..fef27b8 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -19,4 +19,4 @@ spring.h2.console.enabled=true # Encryption Secret for Passcodes (Must be 16, 24, or 32 bytes) bot.encryption.secret=12345678901234567890123456789012 -whatsapp.api-token=EAAc3Wjql00oBSUjYhQ1TZBiNJNmwNlpktCtJpqSLXycDVjWjPiP5dBWHvvFX2Deq1tGXQO2jgVOGChnJk9jsMxLVdxvUs4z7g7xlfcQtAPYiHwvQbPVSbGTTCBvjv2IvHf7Ql2iNBKL72GZCrxAJrQpmjHUG5sbQkVWmMpNfwgI52iDBw0BifU1QhQHFEOnQfiPu19KUp3hJOg6XkUbbXyfBKvbSkZB57xj0XHMv6wgpqDrQwAACOjSlSPrNEC3eZB9DbZAqWYnPkJCsX99Pz3GhA +whatsapp.api-token=EAAc3Wjql00oBSfSg58drijkqefLn47aI2vYcl8r6GxuwC5vJRt2G8Std3ynf3KnKp9SyuwKXAQfr2Q7hYrNOE66t3dRcS3GAWFbDbikrmvXkbjpIh5V9QNVGI2T4V1VeJAj9d6HMsiHZCAJujaIVLdLleIPti4ok9brnNf1BPwBZA8SZAQ06OUcbSba3SZAQhcoFsh1Utz1g9PLCqsl8Fh9H8ynVJagAxMYqF86sV7fEHKpG6eCp2EkRtfOoE7XbL5AurGkiZA5ZBHiSZCOOujplm3cmgZDZD