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
8 changes: 4 additions & 4 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ jobs:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: "11"
- uses: github/codeql-action/init@v3
- uses: github/codeql-action/init@v4
with:
languages: java
- run: mvn -B -DskipTests package
- uses: github/codeql-action/analyze@v3
- uses: github/codeql-action/analyze@v4
9 changes: 5 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ jobs:
steps:
- name: Checkout code
if: github.event.inputs.perform_version == ''
uses: actions/checkout@v2
uses: actions/checkout@v7
- name: Checkout full repository
# Required when performing an existing release.
if: github.event.inputs.perform_version != ''
uses: actions/checkout@v2
uses: actions/checkout@v7
with:
fetch-depth: '0'
- name: Setup git user
Expand All @@ -38,9 +38,10 @@ jobs:
git config --global user.name "Kill Bill core team"
git config --global url."https://${BUILD_USER}:${BUILD_TOKEN}@github.com/".insteadOf "git@github.com:"
- name: Configure Java
uses: actions/setup-java@v1
uses: actions/setup-java@v6
with:
java-version: 11
distribution: temurin
- name: Download Java dependencies
# We do as much as we can, but it may not be enough (https://issues.apache.org/jira/browse/MDEP-82)
run: |
Expand All @@ -59,7 +60,7 @@ jobs:
# Will be pushed as part of the release process, only if the release is successful
git commit -m "pom.xml: update killbill-oss-parent to ${{ github.event.inputs.parent_version }}"
- name: Configure settings.xml for release
uses: actions/setup-java@v3
uses: actions/setup-java@v6
with:
java-version: 11
distribution: temurin
Expand Down
4 changes: 2 additions & 2 deletions docs/aviate-integration.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Aviate integration (0.1.0)

Kill Bill feedback on this plugin asks for enterprise parity with Aviate deployment patterns. Shipped in **0.1.0**.
Kill Bill feedback on [PR #1](https://github.com/killbill/killbill-kintsugi-plugin/pull/1) asks for enterprise parity with Aviate deployment patterns. Shipped in **0.1.0**.

## Deployment paths (Kill Bill team guidance)

Expand Down Expand Up @@ -40,7 +40,7 @@ Documented in `InvoicePluginPropertyNames` for coordination with the Aviate plug
## Planned follow-ups

* **Aviate Catalog tax codes** — resolve plan → product → Aviate tax code from tenant config or API.
* **Repairs and return documents** — `ITEM_ADJ` / `REPAIR_ADJ` as return lines; stable document IDs.
* ~~**Repairs and return documents** — `ITEM_ADJ` / `REPAIR_ADJ` as return lines; stable document IDs.~~ Shipped MVP: return estimate with `{invoiceId}:adj-return`, negative TAX linked to adj item; lenient skip when linked item missing. Follow-ups: TaxDate from original invoice, strict mode, CREDIT_ADJ policy.

## References

Expand Down
16 changes: 12 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,20 +74,28 @@
</properties>

<dependencies>
<!-- Align jackson-* together: databind 2.22 needs annotations with JsonSerializeAs;
parent still manages annotations at 2.13.x which breaks tests if only databind is bumped.
jackson-annotations uses major.minor only (2.22), not 2.22.2. -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.22</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.18.6</version>
<version>2.22.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.6</version>
<version>2.22.2</version>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>2.27.2</version>
<version>3.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down Expand Up @@ -214,7 +222,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<version>3.6.0</version>
<configuration>
<groups>fast</groups>
</configuration>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,18 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/** Invoice plugin that delegates tax calculation to the Kintsugi tax API. */
/**
* Invoice plugin that delegates tax calculation to the Kintsugi tax API.
*
* <p>Sales path: taxable lines → estimate/commit → positive TAX.
* Return path: untaxed {@code ITEM_ADJ}/{@code REPAIR_ADJ} → return
* estimate → negative TAX linked to the adj item.
*/
public final class KintsugiInvoicePluginApi extends PluginInvoicePluginApi {

private static final Logger LOGGER = LoggerFactory.getLogger(KintsugiInvoicePluginApi.class);
Expand Down Expand Up @@ -80,9 +87,11 @@ public KintsugiAdditionalItemsResult getAdditionalInvoiceItems(
return emptyResult();
}

if (InvoiceTaxIdempotency.allTaxableItemsAlreadyTaxed(invoice)) {
final boolean salesNeeded = !InvoiceTaxIdempotency.allTaxableItemsAlreadyTaxed(invoice);
final List<InvoiceItem> untaxedAdjustments = InvoiceTaxIdempotency.untaxedAdjustmentItems(invoice);
if (!salesNeeded && untaxedAdjustments.isEmpty()) {
LOGGER.debug(
"Skipping Kintsugi tax for invoice account {} — taxable lines already have TAX items",
"Skipping Kintsugi tax for invoice account {} — sales and adjustments already taxed",
invoice.getAccountId());
return emptyResult();
}
Expand All @@ -99,28 +108,49 @@ public KintsugiAdditionalItemsResult getAdditionalInvoiceItems(
config,
tenantApiKey,
tenant.getApiSecret());
final ObjectNode requestBody = InvoiceRequestMapper.toEstimateRequest(
invoice,
account,
dryRun,
invoiceContext.getTenantId() != null ? invoiceContext.getTenantId().toString() : null,
taxMetadata);
final String tenantIdStr = invoiceContext.getTenantId() != null
? invoiceContext.getTenantId().toString()
: null;

final KintsugiTaxClient client = new KintsugiTaxClient(
config.getKintsugiUrl(),
config.getHmacSecret(),
tenantApiKey);

final List<KintsugiTaxClient.TaxLineResult> taxLines =
client.estimate(requestBody, !dryRun);

final Map<UUID, InvoiceItem> taxableById = TaxItemMapper.indexTaxableItems(invoice);
final List<InvoiceItem> taxItems = TaxItemMapper.toTaxItems(invoice, taxLines, taxableById);
final Map<UUID, InvoiceItem> itemsById = TaxItemMapper.indexTaxableItems(invoice);
final List<InvoiceItem> taxItems = new ArrayList<>();

if (salesNeeded) {
final ObjectNode salesBody = InvoiceRequestMapper.toEstimateRequest(
invoice, account, dryRun, tenantIdStr, taxMetadata);
// Defense: never POST an empty sales document (adj/credit-only invoices).
if (salesBody.path("documents").path(0).path("line_items").size() > 0) {
final List<KintsugiTaxClient.TaxLineResult> salesTaxLines =
client.estimate(salesBody, !dryRun);
taxItems.addAll(TaxItemMapper.toTaxItems(invoice, salesTaxLines, itemsById));
}
}

if (!untaxedAdjustments.isEmpty()) {
final ObjectNode returnBody = InvoiceRequestMapper.toReturnEstimateRequest(
invoice, account, dryRun, tenantIdStr, taxMetadata, untaxedAdjustments);
if (returnBody != null) {
final List<KintsugiTaxClient.TaxLineResult> returnTaxLines =
client.estimate(returnBody, !dryRun);
taxItems.addAll(TaxItemMapper.toTaxItems(invoice, returnTaxLines, itemsById));
} else {
LOGGER.debug(
"No return-tax lines for invoice account {} after lenient skip of unlinked adjs",
invoice.getAccountId());
}
}

LOGGER.info(
"Kintsugi returned {} tax line(s) for invoice account {}",
"Kintsugi returned {} tax line(s) for invoice account {} (salesNeeded={}, adjReturns={})",
taxItems.size(),
invoice.getAccountId());
invoice.getAccountId(),
salesNeeded,
untaxedAdjustments.size());

return new KintsugiAdditionalItemsResult(taxItems);
} catch (InvoicePluginApiRetryException e) {
Expand Down
Loading
Loading