Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ repositories {
}

dependencies {
implementation("cz.insuit:moneta-api-spring-boot-starter:0.1.0") // or -play, -micronaut, -quarkus, or moneta-api alone
implementation("cz.insuit:moneta-api-spring-boot-starter:0.2.0") // or -play, -micronaut, -quarkus, or moneta-api alone
}

// gh's token, or nothing when gh is missing or signed out: the build then gets GitHub's 401, not a crash.
Expand Down Expand Up @@ -71,7 +71,7 @@ fun gh(vararg args: String): Provider<String> =
<dependency>
<groupId>cz.insuit</groupId>
<artifactId>moneta-api-spring-boot-starter</artifactId>
<version>0.1.0</version>
<version>0.2.0</version>
</dependency>
```

Expand All @@ -80,4 +80,4 @@ and in `~/.m2/settings.xml` a `<server>` with the same `id`, your GitHub user an
## Snapshots

None are published: only a tagged release is. To try what is not released yet, `make publish-local` in a checkout
installs every jar into `~/.m2` as `0.1.0-SNAPSHOT`, where `mavenLocal()` finds them.
installs every jar into `~/.m2` as `0.3.0-SNAPSHOT`, where `mavenLocal()` finds them.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Maven Central.
| [Roadmap](.docs/plan/roadmap.md) | the working plan: what is still unknown, what comes next, what is not covered |
| [Development](.docs/development.md) | build, tests, `make dev` against a real account |
| [Example café](examples/cafe/README.md) | bills as QR payments, found paid; try it at http://localhost:8080/scalar after `make dev` |
| [llms.txt](llms.txt) | the setup and the mistakes to avoid, for a coding assistant adding the library to a project |

## What it covers

Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ plugins {
val testsJdk = providers.gradleProperty("testsJdk")

// A tag vX.Y.Z releases X.Y.Z (-PreleaseVersion, set by `make publish`); anything else is the next snapshot.
val releaseVersion = providers.gradleProperty("releaseVersion").orElse("0.1.0-SNAPSHOT")
val releaseVersion = providers.gradleProperty("releaseVersion").orElse("0.3.0-SNAPSHOT")

allprojects {
group = "cz.insuit"
Expand Down
48 changes: 48 additions & 0 deletions llms.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# moneta-api-java

> A Java 21+ and Kotlin client for MONETA API, the free API of the Czech bank MONETA Money Bank for business
> accounts: it reads accounts, balances and transactions with a token the account holder issues in Internet Banka.
> Modules for Spring Boot, Play, Micronaut and Quarkus. Published on GitHub Packages (group `cz.insuit`), not Maven
> Central. Pre-1.0: read records by accessor, never by record pattern.

Setting it up in a project:

- Add the module for the project's framework, or `cz.insuit:moneta-api` without one; each module's README has
the Gradle and Maven lines, sbt for Play, and a prompt for a coding assistant. The framework modules bring
`moneta-api` with them; do not add both.
- Add the GitHub Packages repository `https://maven.pkg.github.com/landsman/moneta-api-java`. It asks for a token
even for a public package (`read:packages`). Never write a token into the build file or the repository.
- The MONETA token comes from the environment: `moneta.token` (Spring Boot, Play, Micronaut) or
`quarkus.moneta.token` (Quarkus), which also provides an `AccessToken` bean.
- One `MonetaClient` per application (framework modules provide it as a bean). Bind a token with
`moneta.with(token)`, then `transactions().between(from, to)`, optionally `.incoming()`, `.outgoing()`,
`.transfers()`, `.cards()`, `.booked()`, `.pending()`, and `.list()`.

Mistakes to avoid:

- `entryReference` changes when a pending movement is booked: deduplicating by it counts a payment twice.
- Movements have dates only, no time of day.
- `amount()` is signed: negative is money out.
- `Transaction.Method` gains constants: keep a `default` branch.
- A read is all or nothing across pages; read long ranges month by month.
- The library filters by method, direction and status in memory: only a shorter date range makes a read cheaper.
- Do not return a `Transaction` from a controller as JSON: `raw()` carries the counterparty's name and account.

## Docs

- [Installing](https://github.com/landsman/moneta-api-java/blob/main/.docs/install.md): the repository block and the token, for Gradle and Maven
- [Using the client](https://github.com/landsman/moneta-api-java/blob/main/.docs/usage.md): calls, filtering, more than one account, failures
- [How MONETA API behaves](https://github.com/landsman/moneta-api-java/blob/main/.docs/moneta-api.md): pending and booked movements, dates, ids, errors

## Modules

- [moneta-api](https://github.com/landsman/moneta-api-java/blob/main/modules/moneta-api/README.md): the client alone
- [Spring Boot starter](https://github.com/landsman/moneta-api-java/blob/main/modules/moneta-api-spring-boot-starter/README.md): `moneta.*` in application.yml, a `MonetaClient` bean
- [Play module](https://github.com/landsman/moneta-api-java/blob/main/modules/moneta-api-play/README.md): `moneta.*` in application.conf; calls block, run them on their own pool
- [Micronaut module](https://github.com/landsman/moneta-api-java/blob/main/modules/moneta-api-micronaut/README.md): `moneta.*`, a `MonetaClient` bean
- [Quarkus extension](https://github.com/landsman/moneta-api-java/blob/main/modules/moneta-api-quarkus/README.md): `quarkus.moneta.*`, JVM and native

## Optional

- [Kotlin and Spring Boot](https://github.com/landsman/moneta-api-java/blob/main/.docs/compatibility.md): Spring Boot 3 before 3.5.13 needs one dependency pinned; native images
- [Roadmap](https://github.com/landsman/moneta-api-java/blob/main/.docs/plan/roadmap.md): what is still unknown
102 changes: 100 additions & 2 deletions modules/moneta-api-micronaut/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,113 @@
Micronaut wiring for the client: add the dependency, set `moneta.*`, inject `MonetaClient`. One jar for
Micronaut 4.10 and 5; [testing/micronaut](../../testing/README.md) checks each.

## Install

```kotlin
// build.gradle.kts
dependencies {
implementation("cz.insuit:moneta-api-micronaut:0.2.0")
}
```

<details><summary>Maven</summary>

```xml
<!-- in <dependencies> -->
<dependency>
<groupId>cz.insuit</groupId>
<artifactId>moneta-api-micronaut</artifactId>
<version>0.2.0</version>
</dependency>
```

</details>

GitHub Packages asks for a token even for a public package: the repository block and the token are in
[Installing](../../.docs/install.md).

## With a coding assistant

Paste this into it:

```text
Add the MONETA API client to this Micronaut project.

1. Read https://github.com/landsman/moneta-api-java/blob/main/llms.txt and https://github.com/landsman/moneta-api-java/blob/main/modules/moneta-api-micronaut/README.md first. Do not guess the API.
2. Dependency: cz.insuit:moneta-api-micronaut:0.2.0 from GitHub Packages, with Gradle or Maven, as
https://github.com/landsman/moneta-api-java/blob/main/.docs/install.md shows. Never write a token into the project.
3. Configuration: moneta.token: ${MONETA_TOKEN} in application.yml.
4. Code: inject MonetaClient and AccessToken. Bind the token with moneta.with(token), read with
transactions().between(from, to), narrowed by incoming(), outgoing(), transfers(),
cards(), booked() or pending(), then list().
5. Act on booked movements only, unless I ask for pending ones.
```

## Configure

```yaml
moneta:
token: ${MONETA_TOKEN}
```

That is the whole setup for an application reading one account: inject `MonetaClient` and `AccessToken`. One
serving many merchants leaves `moneta.token` unset and passes each one's token with the call. Keep the token in
serving many account holders leaves `moneta.token` unset and passes each one's token with the call. Keep the token in
the environment, never in a committed file.

```java
import cz.insuit.moneta.AccessToken;
import cz.insuit.moneta.AccountHolder;
import cz.insuit.moneta.MonetaClient;
import cz.insuit.moneta.Transaction;
import jakarta.inject.Singleton;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.List;

@Singleton
public class Payments {
private final AccountHolder me;

public Payments(MonetaClient moneta, AccessToken token) {
this.me = moneta.with(token);
}

/** Money received over the last week, booked. */
public List<Transaction> receivedThisWeek() {
var today = LocalDate.now(ZoneId.of("Europe/Prague"));
return me.transactions().between(today.minusDays(7), today).incoming().booked().list();
}
}
```

<details><summary>Kotlin</summary>

```kotlin
import cz.insuit.moneta.AccessToken
import cz.insuit.moneta.MonetaClient
import cz.insuit.moneta.Transaction
import jakarta.inject.Singleton
import java.time.LocalDate
import java.time.ZoneId

@Singleton
class Payments(moneta: MonetaClient, token: AccessToken) {
private val me = moneta.with(token)

/** Money received over the last week, booked. */
fun receivedThisWeek(): List<Transaction> {
val today = LocalDate.now(ZoneId.of("Europe/Prague"))
return me.transactions().between(today.minusDays(7), today).incoming().booked().list()
}
}
```

</details>

It reads the only account the token opens and throws `IllegalStateException` when there are more; then, or with
one token per account holder, see [More than one account](../../.docs/usage.md#more-than-one-account).
Without `moneta.token` there is no `AccessToken` bean to inject.

A `MonetaClient` bean of the application's own replaces the one built here. Like every Micronaut bean, the client
is built when first injected, not at startup, and closed with the context. A token or partner value it would refuse
fails when `moneta.*` is bound; to have that stop the start, without building the client, opt in as Micronaut
Expand Down Expand Up @@ -98,7 +196,7 @@ None is needed against production. The names are the Spring Boot starter's.
|----------|---------|---|
| `moneta.base-uri` | production, `MonetaClient.PRODUCTION` | the sandbox's host once the portal gives it |
| `moneta.request-timeout` | the client's, 30 s | one call to MONETA: `20s` |
| `moneta.partner.identification-number`, `.business-name`, `.application-name` | — | the registered application's headers, all three or none, ASCII only |
| `moneta.partner.identification-number`, `.business-name`, `.application-name` | none | the registered application's headers, all three or none, ASCII only |

MONETA does not ask for the partner headers: every call so far went through without ([how it
behaves](../../.docs/moneta-api.md)). Likely for an application approved on the developer portal; set them once the
Expand Down
103 changes: 94 additions & 9 deletions modules/moneta-api-play/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,102 @@ A Play Framework module for the client: add the dependency, set `moneta.*`, inje
`reference.conf` enables it; nothing else to register. Works on Play 3.0 (Scala 2.13 and 3) and 2.9;
[testing/play](../../testing/README.md) checks each.

## Install

```scala
// build.sbt
resolvers += "GitHub Packages" at "https://maven.pkg.github.com/landsman/moneta-api-java"
credentials += Credentials("GitHub Package Registry", "maven.pkg.github.com", "gh", sys.env.getOrElse("GITHUB_TOKEN", ""))
libraryDependencies += "cz.insuit" % "moneta-api-play" % "0.2.0"
```

`GITHUB_TOKEN` holds a token with `read:packages`; Gradle and Maven below.

```kotlin
// build.gradle.kts
dependencies {
implementation("cz.insuit:moneta-api-play:0.2.0")
}
```

<details><summary>Maven</summary>

```xml
<!-- in <dependencies> -->
<dependency>
<groupId>cz.insuit</groupId>
<artifactId>moneta-api-play</artifactId>
<version>0.2.0</version>
</dependency>
```

</details>

GitHub Packages asks for a token even for a public package: the repository block and the token are in
[Installing](../../.docs/install.md).

## With a coding assistant

Paste this into it:

```text
Add the MONETA API client to this Play Framework project.

1. Read https://github.com/landsman/moneta-api-java/blob/main/llms.txt and https://github.com/landsman/moneta-api-java/blob/main/modules/moneta-api-play/README.md first. Do not guess the API.
2. Dependency: cz.insuit:moneta-api-play:0.2.0 from GitHub Packages, with sbt, Gradle or Maven, as
https://github.com/landsman/moneta-api-java/blob/main/.docs/install.md shows. Never write a token into the project.
3. Configuration: moneta.token = ${?MONETA_TOKEN} in application.conf.
4. Code: inject MonetaClient and AccessToken (javax.inject), and run the calls on a pool of their own, not Play's default one. Bind the token with moneta.with(token), read with
transactions().between(from, to), narrowed by incoming(), outgoing(), transfers(),
cards(), booked() or pending(), then list().
5. Act on booked movements only, unless I ask for pending ones.
```

## Configure

```hocon
# application.conf
moneta.token = ${?MONETA_TOKEN}
```

That is the whole setup for an application reading one account: inject `MonetaClient` and `AccessToken`. One
serving many merchants leaves `moneta.token` unset and passes each one's token with the call. Keep the token in
serving many account holders leaves `moneta.token` unset and passes each one's token with the call. Keep the token in
the environment, never in a committed file.

```java
import cz.insuit.moneta.AccessToken;
import cz.insuit.moneta.AccountHolder;
import cz.insuit.moneta.MonetaClient;
import cz.insuit.moneta.Transaction;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;

@Singleton
public class Payments {
private final AccountHolder me;

@Inject
public Payments(MonetaClient moneta, AccessToken token) {
this.me = moneta.with(token);
}

/** Money received over the last week, booked. */
public List<Transaction> receivedThisWeek() {
var today = LocalDate.now(ZoneId.of("Europe/Prague"));
return me.transactions().between(today.minusDays(7), today).incoming().booked().list();
}
}
```

It reads the only account the token opens and throws `IllegalStateException` when there are more; then, or with
one token per account holder, see [More than one account](../../.docs/usage.md#more-than-one-account).
Without `moneta.token` there is no `AccessToken` bean to inject.

The call blocks: run it on [a pool of its own](#calls-block-give-them-a-pool-of-their-own), not Play's default one.

The client is a singleton built when the application starts, so a token it would refuse stops the start, and it is
closed when the application stops, a dev-mode reload included.

Expand Down Expand Up @@ -44,18 +131,16 @@ public class BlockingExecutor implements Executor {

```java
public class PaymentsController extends Controller {
private final MonetaClient moneta;
private final AccessToken token;
private final Payments payments;
private final BlockingExecutor blocking;
private final ClassLoaderExecutionContext http;

@Inject
public PaymentsController(
MonetaClient moneta, AccessToken token, BlockingExecutor blocking, ClassLoaderExecutionContext http) { … }
public PaymentsController(Payments payments, BlockingExecutor blocking, ClassLoaderExecutionContext http) { … }

public CompletionStage<Result> incoming() {
return CompletableFuture.supplyAsync(() -> moneta.incoming(token, accountId, from, to), blocking)
.thenApplyAsync(ts -> ok(views.html.incoming.render(ts)), http.current()); // back off the blocking pool
public CompletionStage<Result> received() {
return CompletableFuture.supplyAsync(payments::receivedThisWeek, blocking)
.thenApplyAsync(ts -> ok(views.html.received.render(ts)), http.current()); // back off the blocking pool
}
}
```
Expand Down Expand Up @@ -126,7 +211,7 @@ None is needed against production. The keys are the Spring Boot starter's.
|-----|---------|---|
| `moneta.base-uri` | production, `MonetaClient.PRODUCTION` | the sandbox's host once the portal gives it |
| `moneta.request-timeout` | the client's, 30 s | one call to MONETA, as a HOCON duration: `20s` |
| `moneta.partner.identification-number`, `.business-name`, `.application-name` | — | the registered application's headers, all three or none, ASCII only |
| `moneta.partner.identification-number`, `.business-name`, `.application-name` | none | the registered application's headers, all three or none, ASCII only |

MONETA does not ask for the partner headers: every call so far went through without ([how it
behaves](../../.docs/moneta-api.md)). Likely for an application approved on the developer portal; set them once the
Expand Down
Loading
Loading