Fees and structures added

This commit is contained in:
RIT Services
2026-08-20 06:43:00 +00:00
parent 9d83dec681
commit 6e52735484
9 changed files with 909 additions and 44 deletions

View File

@@ -1215,6 +1215,11 @@ Cache-Control: no-store
| GET | `/api/student/results?semester=N` | Yes | | GET | `/api/student/results?semester=N` | Yes |
| POST | `/api/student/results` | Yes | | POST | `/api/student/results` | Yes |
| GET | `/api/student/fees` | 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 |
--- ---

Binary file not shown.

View File

@@ -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.SingleFileStore.open(SingleFileStore.java:81)
at org.h2.mvstore.MVStore.<init>(MVStore.java:291) at org.h2.mvstore.MVStore.<init>(MVStore.java:291)
... 151 more ... 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.<init>(MVStore.java:296)
at org.h2.mvstore.MVStore$Builder.open(MVStore.java:2164)
at org.h2.mvstore.db.Store.<init>(Store.java:133)
at org.h2.engine.Database.<init>(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.<init>(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.<init>(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.<init>(Database.java:44)
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.getDatabase(InFlightMetadataCollectorImpl.java:251)
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.<init>(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.<init>(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.<init>(MVStore.java:296)
at org.h2.mvstore.MVStore$Builder.open(MVStore.java:2164)
at org.h2.mvstore.db.Store.<init>(Store.java:133)
at org.h2.engine.Database.<init>(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.<init>(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.<init>(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.<init>(DatabaseInformationImpl.java:63)
at org.hibernate.tool.schema.extract.internal.CachingDatabaseInformationImpl.<init>(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.<init>(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.<init>(MVStore.java:291)
... 151 more

View File

@@ -75,6 +75,13 @@ public class WebhookController {
String messageType = messageNode.get("type").asText(); String messageType = messageNode.get("type").asText();
UserState currentState = sessionService.getUserState(fromPhoneNumber); 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); boolean isAuthenticated = sessionService.isAuthenticated(fromPhoneNumber);
// Global reset command // Global reset command
@@ -166,7 +173,7 @@ public class WebhookController {
messageService.sendMessage(phoneNumber, "This menu selection is no longer active."); messageService.sendMessage(phoneNumber, "This menu selection is no longer active.");
studentMenuHandler.sendMainMenu(phoneNumber); 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 // They clicked a receipt download option
if (currentState == UserState.AWAITING_RECEIPT_SELECTION) { if (currentState == UserState.AWAITING_RECEIPT_SELECTION) {
studentMenuHandler.handleReceiptSelection(phoneNumber, selectedListId); studentMenuHandler.handleReceiptSelection(phoneNumber, selectedListId);
@@ -191,6 +198,16 @@ public class WebhookController {
return; 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) { if (currentState == UserState.AWAITING_GRIEVANCE_CATEGORY) {
grievanceHandler.handleCategorySelection(phoneNumber, selectedButtonId); grievanceHandler.handleCategorySelection(phoneNumber, selectedButtonId);
} else { } else {

View File

@@ -97,30 +97,29 @@ public class StudentMenuHandler {
sendSemesterSelection(phoneNumber, token); sendSemesterSelection(phoneNumber, token);
break; break;
case "student_fee_details": case "student_fee_details": {
String rawFees = imsApiService.getFees(token); String rawFees = imsApiService.getFees(token);
String rawExamFees = imsApiService.getExamFees(token); String bodyText = MessageFormatter.formatAcademicFeesOnly(rawFees);
String bodyText = MessageFormatter.formatFeesAndExamFees(rawFees, rawExamFees);
List<Map<String, String>> receiptRows = MessageFormatter.extractReceiptRows(rawExamFees); List<Map<String, String>> feeButtons = new ArrayList<>();
Map<String, String> examBtn = new HashMap<>();
examBtn.put("id", "student_exam_fees");
examBtn.put("title", "Exam Fees");
feeButtons.add(examBtn);
if (!receiptRows.isEmpty()) { Map<String, String> historyBtn = new HashMap<>();
// Max 10 items in a list. Limit to 10 if necessary. historyBtn.put("id", "student_payment_history");
if (receiptRows.size() > 10) receiptRows = receiptRows.subList(0, 10); historyBtn.put("title", "Payment History");
feeButtons.add(historyBtn);
sessionService.setUserState(phoneNumber, UserState.AWAITING_RECEIPT_SELECTION); if (bodyText.length() > 1020) {
bodyText = bodyText.substring(0, 1017) + "...";
// Meta API interactive list body limit is 1024 characters
if (bodyText.length() > 1024) bodyText = bodyText.substring(0, 1021) + "...";
Map<String, Object> listMessage = WhatsAppMessageBuilder.buildListMessage(
"Receipts", bodyText, "Download Receipt", receiptRows
);
messageService.sendInteractiveMessage(phoneNumber, listMessage);
} else {
sendResponseWithMenu(phoneNumber, bodyText);
} }
Map<String, Object> feeButtonMessage = WhatsAppMessageBuilder.buildButtonMessage(bodyText, feeButtons);
messageService.sendInteractiveMessage(phoneNumber, feeButtonMessage);
break; break;
}
case "student_raise_grievance": case "student_raise_grievance":
grievanceHandler.startGrievanceFlow(phoneNumber); grievanceHandler.startGrievanceFlow(phoneNumber);
@@ -209,15 +208,31 @@ public class StudentMenuHandler {
} }
try { try {
String historyId = selectedOptionId.replace("receipt_", ""); byte[] pdfBytes = null;
byte[] pdfBytes = imsApiService.getExamFeeReceiptPdf(token, historyId); 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) { if (pdfBytes != null) {
String fileName = "Receipt_" + historyId + ".pdf";
String mediaId = messageService.uploadMedia(pdfBytes, fileName, "application/pdf"); String mediaId = messageService.uploadMedia(pdfBytes, fileName, "application/pdf");
if (mediaId != null) { 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."); sendResponseWithMenu(phoneNumber, "Select an option from the menu below.");
} else { } else {
sendResponseWithMenu(phoneNumber, "Failed to upload the receipt document to WhatsApp."); 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."); 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<String, String> idMappings = new HashMap<>();
List<Map<String, String>> examRows = MessageFormatter.extractReceiptRows(rawExamFees, idMappings);
List<Map<String, String>> acadRows = MessageFormatter.extractAcademicReceiptRows(rawFeesHistory, idMappings);
List<Map<String, String>> consRows = MessageFormatter.extractConsolidatedReceiptRows(rawFeesHistory, idMappings);
sessionService.storeReceiptIdMappings(phoneNumber, idMappings);
List<Map<String, String>> 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<String, Object> listMessage = WhatsAppMessageBuilder.buildListMessage(
"Receipts", formattedHistory, "Download Receipt", receiptRows
);
messageService.sendInteractiveMessage(phoneNumber, listMessage);
} else {
sendResponseWithMenu(phoneNumber, formattedHistory);
}
}
private void sendResponseWithMenu(String phoneNumber, String bodyText) { private void sendResponseWithMenu(String phoneNumber, String bodyText) {
// Meta API interactive list body limit is 1024 characters // Meta API interactive list body limit is 1024 characters
if (bodyText.length() > 1024) { if (bodyText.length() > 1024) {

View File

@@ -134,7 +134,15 @@ public class ImsApiService {
} }
public byte[] getExamFeeReceiptPdf(String token, String historyId) { 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(); HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token); headers.setBearerAuth(token);
@@ -186,4 +194,60 @@ public class ImsApiService {
} }
return "Student"; 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<String> request = new HttpEntity<>(headers);
try {
ResponseEntity<byte[]> 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<String> request = new HttpEntity<>(headers);
try {
ResponseEntity<byte[]> 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;
}
} }

View File

@@ -42,6 +42,14 @@ public class SessionService {
userStates.put(phoneNumber, state); 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) { public void resetState(String phoneNumber) {
userStates.remove(phoneNumber); userStates.remove(phoneNumber);
} }
@@ -104,6 +112,20 @@ public class SessionService {
return name; return name;
} }
private final Map<String, Map<String, String>> receiptIdMappings = new ConcurrentHashMap<>();
public void storeReceiptIdMappings(String phoneNumber, Map<String, String> mappings) {
receiptIdMappings.put(phoneNumber, mappings);
}
public String getReceiptIdMapping(String phoneNumber, String shortId) {
Map<String, String> mappings = receiptIdMappings.get(phoneNumber);
if (mappings != null) {
return mappings.get(shortId);
}
return null;
}
public void setTempLoginId(String phoneNumber, String tempId) { public void setTempLoginId(String phoneNumber, String tempId) {
temporaryLoginIds.put(phoneNumber, tempId); temporaryLoginIds.put(phoneNumber, tempId);
} }

View File

@@ -51,10 +51,9 @@ public class MessageFormatter {
if (!foundData) { if (!foundData) {
sb.append("Details not available in this format.\n"); sb.append("Details not available in this format.\n");
sb.append("\n*DEBUG RAW ATTENDANCE JSON:*\n").append(rawAttendance).append("\n");
} }
} catch (Exception e) { } 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) { if (!foundData) {
sb.append("Details not available in this format.\n"); sb.append("Details not available in this format.\n");
sb.append("\n*DEBUG RAW LEAVES JSON:*\n").append(rawLeaves).append("\n");
} }
} catch (Exception e) { } catch (Exception e) {
sb.append("⚠️ Error formatting leaves data.\nRAW JSON: ").append(rawLeaves).append("\n"); sb.append("⚠️ Error formatting leaves data.\n");
} }
} }
@@ -482,30 +480,385 @@ public class MessageFormatter {
return sb.toString(); return sb.toString();
} }
public static java.util.List<java.util.Map<String, String>> 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<java.util.Map.Entry<String, JsonNode>> fields = data.fields();
while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> 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<java.util.Map<String, String>> extractReceiptRows(String rawExamFees, java.util.Map<String, String> idMappings) {
java.util.List<java.util.Map<String, String>> rows = new java.util.ArrayList<>(); java.util.List<java.util.Map<String, String>> rows = new java.util.ArrayList<>();
if (rawExamFees == null) return rows; if (rawExamFees == null) return rows;
try { try {
JsonNode root = mapper.readTree(rawExamFees); JsonNode root = mapper.readTree(rawExamFees);
if (root.has("data") && root.get("data").isArray()) { if (root.has("data")) {
for (JsonNode fee : root.get("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<java.util.Map.Entry<String, JsonNode>> fields = historyNode.fields();
while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> field = fields.next();
JsonNode arrayNode = field.getValue();
if (arrayNode.isArray()) {
for (JsonNode fee : arrayNode) {
if (fee.has("history_id") || fee.has("id")) { if (fee.has("history_id") || fee.has("id")) {
String id = fee.has("history_id") ? fee.get("history_id").asText() : fee.get("id").asText(); 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 amount = fee.has("paid_amt") ? fee.get("paid_amt").asText() : (fee.has("amount") ? fee.get("amount").asText() : "");
String date = fee.has("date") ? fee.get("date").asText() : ""; String date = fee.has("paid_date") ? fee.get("paid_date").asText() : (fee.has("date") ? fee.get("date").asText() : "");
java.util.Map<String, String> row = new java.util.HashMap<>(); java.util.Map<String, String> row = new java.util.HashMap<>();
row.put("id", "receipt_" + id); String shortId = "exam_" + (rows.size() + 1);
row.put("title", "Receipt " + id); 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)); row.put("description", "₹" + amount + (date.isEmpty() ? "" : " on " + date));
rows.add(row); rows.add(row);
} }
} }
} }
}
}
}
} catch (Exception e) {} } catch (Exception e) {}
return rows; return rows;
} }
public static java.util.List<java.util.Map<String, String>> extractAcademicReceiptRows(String rawFeesHistory, java.util.Map<String, String> idMappings) {
java.util.List<java.util.Map<String, String>> 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<java.util.Map.Entry<String, JsonNode>> fields = historyNode.fields();
while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> 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<String, String> 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<String, String> 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<java.util.Map<String, String>> extractConsolidatedReceiptRows(String rawFeesHistory, java.util.Map<String, String> idMappings) {
java.util.List<java.util.Map<String, String>> 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<String, String> 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<java.util.Map.Entry<String, JsonNode>> fields = historyNode.fields();
while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> 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<java.util.Map.Entry<String, JsonNode>> fields = historyNode.fields();
while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> 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) { private static String getNumberEmoji(int num) {
switch (num) { switch (num) {
case 1: return "1️⃣"; case 1: return "1️⃣";

View File

@@ -19,4 +19,4 @@ spring.h2.console.enabled=true
# Encryption Secret for Passcodes (Must be 16, 24, or 32 bytes) # Encryption Secret for Passcodes (Must be 16, 24, or 32 bytes)
bot.encryption.secret=12345678901234567890123456789012 bot.encryption.secret=12345678901234567890123456789012
whatsapp.api-token=EAAc3Wjql00oBSUjYhQ1TZBiNJNmwNlpktCtJpqSLXycDVjWjPiP5dBWHvvFX2Deq1tGXQO2jgVOGChnJk9jsMxLVdxvUs4z7g7xlfcQtAPYiHwvQbPVSbGTTCBvjv2IvHf7Ql2iNBKL72GZCrxAJrQpmjHUG5sbQkVWmMpNfwgI52iDBw0BifU1QhQHFEOnQfiPu19KUp3hJOg6XkUbbXyfBKvbSkZB57xj0XHMv6wgpqDrQwAACOjSlSPrNEC3eZB9DbZAqWYnPkJCsX99Pz3GhA whatsapp.api-token=EAAc3Wjql00oBSfSg58drijkqefLn47aI2vYcl8r6GxuwC5vJRt2G8Std3ynf3KnKp9SyuwKXAQfr2Q7hYrNOE66t3dRcS3GAWFbDbikrmvXkbjpIh5V9QNVGI2T4V1VeJAj9d6HMsiHZCAJujaIVLdLleIPti4ok9brnNf1BPwBZA8SZAQ06OUcbSba3SZAQhcoFsh1Utz1g9PLCqsl8Fh9H8ynVJagAxMYqF86sV7fEHKpG6eCp2EkRtfOoE7XbL5AurGkiZA5ZBHiSZCOOujplm3cmgZDZD