FINERACT-2701: support OffsetTime for savings transactions - #6187
AnvayKharb wants to merge 1 commit into
Conversation
c62facb to
7215a56
Compare
adamsaghy
left a comment
There was a problem hiding this comment.
It looks weird at first...
Let me review a little deeper...
We should really not use LocalDateTime and mess with modifiers (Midnight...)
adamsaghy
left a comment
There was a problem hiding this comment.
-
I’m not keen on introducing a brand new field called
TIMESTAMP(on the database side) andLocalDateTime(on the Java side). Instead, let’s useOffsetTimeto ensure accurate timezone handling. -
The
transaction datefield already exists in thesavings transactionandsavings transferentities, and it’s of typeDATE. Do we really need a user-provided time part? Will this be different from the created date time (OffsetDateTime)?
If we do need a user-provided time part, let’s add a new field that stores as OffsetTime and is optional for backward compatibility.
-
I suggest that we request the transaction date and transaction time as two separate fields. This way, we avoid supporting a single field with different data types.
-
Let’s avoid using
LocalDateTimeand parsing to that data type in all places. Instead, the user should provide the time part with an offset TZ. Is this a viable option?
@AnvayKharb @IOhacker what do you think?
|
Hi @adamsaghy Thanks for the detailed review. I understand your concerns. Using a separate optional transactionTime with timezone information while keeping the existing transactionDate sounds like a cleaner and more backward-compatible approach than introducing a LocalDateTime field. I'll wait for @IOhacker 's thoughts as well, and if we're aligned on this direction, I'll update the implementation accordingly. |
587fa6b to
287c73f
Compare
|
Thanks for the guidance @adamsaghy @IOhacker . I updated the PR to follow the separate optional transaction time approach instead of the earlier combined date-time design. Current implementation:
I also rechecked the diff for newly introduced LocalDateTime, transaction_datetime, transactionDateTime, .atStartOfDay(), midnight conversions, and generic TIMESTAMP usage; none remain in this PR diff. Local validation passed for OpenAPI compatibility, formatting/checkstyle, focused transaction-time tests, Liquibase DDL safety, and Liquibase-only runs against both PostgreSQL and MariaDB with actual column type verification. |
287c73f to
cb7bd37
Compare
| return localTime; | ||
| } | ||
|
|
||
| public static OffsetTime getOffsetTime(final ResultSet rs, final String columnName) throws SQLException { |
There was a problem hiding this comment.
I dont think we need this many options... the field is stored as OffsetTime and fetched as OffsetTime. No need for anything else but reading as OffsetTime...
There was a problem hiding this comment.
Please dont mark as resolved without explanation or changes!
There was a problem hiding this comment.
Hi @adamsaghy I apologize for not explaining, I initially added the conversion to preserve the offset consistently, but since the field is already stored and fetched as OffsetTime, that extra logic isn't needed. I'll simplify it to direct OffsetTime handling.
|
@AnvayKharb @IOhacker Can you help me understand this new field a little better? Is this provided new field will be different then the "created date time"? |
|
Hi @adamsaghy Yes, they're different. createdDateTime is the system timestamp when Fineract persisted the transaction, while transactionTime is the optional business time provided by the user for the transaction. I'll simplify the implementation as suggested by removing the converter/helper and persisting OffsetTime directly. |
cb7bd37 to
59ab286
Compare
16f06b0 to
cff0e0d
Compare
Also please make sure your commits are signed by GPG |
741c512 to
0b07a09
Compare
5a52a8d to
8a14ead
Compare
|
@AnvayKharb Please review the failing checks. |
|
Hi @adamsaghy @IOhacker all the checks are now passing |
|
@AnvayKharb Please review the below concerns: Blocking
Every write path normalizes the time to UTC — fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountTransaction.java, fineract-provider/src/main/java/org/apache/fineract/portfolio/account/domain/AccountTransferTransaction.java, fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionDTO.java, and again in JsonParserHelper.extractOffsetTimeNamed — but transactionDate is still taken verbatim from the client's local calendar. POST { "transactionDate": "27 July 2026", "transactionTime": "02:00:00+05:30" } Combining those, as your own review comment asked for ("transaction date + transaction time together can represent a more precise point in time"), gives 2026-07-27T20:30Z. The actual instant was 2026-07-26T20:30Z — off by a full day. Same class of error for any time whose UTC conversion crosses midnight in either direction. The fix is to stop normalizing and persist the OffsetTime exactly as supplied. timetz keeps the offset, so 2026-07-27 + 02:00:00+05:30 reconstructs the correct instant. Normalizing is what breaks it, and it also makes the round-trip lossy (the client can no longer see the offset they sent). Caveat that pushes back on the design: MySQL's TIME(6) cannot store an offset at all, so "stored as OffsetTime, fetched as OffsetTime" only holds on PostgreSQL. On MySQL Hibernate falls back to NORMALIZE against the JVM default zone. Fineract forces -Duser.timezone=UTC in its run configs so it works today, but it's implicit — worth setting hibernate.timezone.default_storage=NORMALIZE_UTC if you keep MySQL support here.
SavingsAccountTransactionData.templateOnTop() (fineract-core/src/main/java/org/apache/fineract/portfolio/savings/data/SavingsAccountTransactionData.java) rebuilds the object via createData(...) and never copies transactionTime. So GET /v1/savingsaccounts/{id}/transactions/{txnId}?template=true returns transactionTime: null for a transaction that has one (fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java). This is a direct consequence of the withTransactionTime mutator approach — a builder (as you already suggested) wouldn't have this hole.
SavingsAccountTransactionDataValidator.validate() now whitelists transactionTime, and adjustSavingsTransaction (fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java) calls that same validator — but builds its SavingsAccountTransactionDTO without the time. Adjusting a transaction therefore accepts the parameter, drops it, and returns 200. Either wire it through or reject it on that endpoint. Unaddressed from your earlier review
Non-blocking, but still should be review and address
|
8a14ead to
dd2ee74
Compare
|
@adamsaghy
|
Description
This PR adds support for an optional
transactionTimefield for savings transactions usingOffsetTimewhile maintaining full backward compatibility with the existing API.The existing
transactionDatefield remains unchanged. Clients can optionally providetransactionTimein ISO-8601 offset time format (for example,14:30:00+05:30). The provided time is normalized to UTC before persistence.The implementation also propagates the optional transaction time through account transfer flows to ensure consistent transaction handling across savings operations.
Changes
transactionTimesupport usingOffsetTime.Related Issue
FINERACT-2701