Skip to content

Conversation

@Azorlogh
Copy link
Contributor

@Azorlogh Azorlogh commented Dec 1, 2025

Summary by cubic

Adds transaction templates to ledger schemas and lets clients execute transactions by template ID. When schema enforcement is enabled, transactions must use a template; otherwise, postings or plain numscript still work.

  • New Features

    • Schema supports transaction templates (id, description, script, runtime).
    • Templates are validated at schema update; invalid templates return ErrInvalidSchema.
    • CreateTransaction accepts script.template and vars; cannot mix with postings or plain numscript.
    • With enforcement enabled: template is required. Without enforcement or without a schema: postings or plain numscript allowed.
    • Transactions include a template field; events (CommittedTransactions/RevertedTransaction) and responses return it.
    • OpenAPI/SDK/docs updated; storage persists templates with the schema and records transaction.template.
  • Migration

    • If using schemas: include data.transactions (can be empty) and update clients to send script.template (+ vars). Do not send postings or ad‑hoc scripts when enforcement is enabled.
    • If consuming InsertedSchema events: transactions is now included; Committed/Reverted transaction events now include template.

Written for commit 655dbf6. Summary will update automatically on new commits.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 1, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds transaction template support: new template and runtime types, schema Transactions storage and validation, template resolution/injection during transaction creation, script objects gain a template reference, DB migration for transactions and schema, and tests/docs updated for templated scripts.

Changes

Cohort / File(s) Summary
Core template types
internal/transaction_templates.go, internal/transaction_templates.go (ledger pkg), pkg/client/models/components/v2transactiontemplate.go, pkg/client/models/components/runtime.go
Introduces TransactionTemplate / TransactionTemplates and Runtime enum/constants, with validation logic and client model types/accessors.
Schema model & storage
internal/schema.go, internal/storage/bucket/migrations/44-add-schema/up.sql, pkg/client/models/components/v2schema.go, pkg/client/models/components/v2schemadata.go, pkg/client/docs/models/components/v2schemadata.md, pkg/client/docs/models/components/v2schema.md
Adds transactions to schema data/models, persists as jsonb, validates templates on schema creation, and updates docs.
Transaction model & DB
internal/transaction.go, internal/storage/ledger/resource_transactions.go, internal/machine/vm/run.go, pkg/client/models/components/v2transaction.go, pkg/client/docs/models/components/v2transaction.md
Adds Template field to Transaction and V2Transaction, WithTemplate setter, includes template in DB selects, and augments Script with template tag.
Post-transaction script API & tests
pkg/client/models/components/v2posttransaction.go, pkg/client/docs/models/components/v2posttransactionscript.md, pkg/generate/generator.go, test/e2e/*, test/*
Changes V2PostTransactionScript.Plain from string to *string, adds Template *string; updates generator and tests to pass pointer-wrapped strings; updates examples/docs.
Controller & parser logic
internal/controller/ledger/controller_default.go, internal/controller/ledger/controller.go, internal/errors.go
Switches parser/getParser to runtime-based input, resolves/injects template scripts during createTransaction, adds NewErrInvalidSchema helper, and removes duplicate local runtime defs.
API payloads & validation
internal/api/v2/controllers_transactions_create.go, internal/api/bulking/elements.go
Adds template-aware validation (disallow mixed tx types, require at least one type), adjusts runtime typing and RunScript construction logic.
Tests & e2e updates
internal/controller/ledger/controller_default_test.go, test/e2e/api_schema_test.go, test/e2e/api_transactions_create_test.go, test/e2e/api_ledgers_import_test.go, test/e2e/api_idempotency_hit_header_test.go, internal/storage/ledger/schema_test.go
Splits controller tests, injects Transactions in schema tests, asserts Template in created transactions, and updates test payloads to use pointers for Plain.
Docs & SDK examples
docs/api/README.md, pkg/client/docs/sdks/v2/README.md, various pkg/client/docs/models/components/*, pkg/client/docs/models/components/v2transactiontemplate.md
Documents V2TransactionTemplate, Runtime enum, transactions in schema, example payloads using template fields, and updates SDK examples to reference templates.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API
    participant Controller
    participant Parser
    participant SchemaStore
    participant Database

    Note over Client,SchemaStore: Insert schema with transaction templates
    Client->>API: InsertSchema(SchemaData{Transactions: {...}})
    API->>Controller: InsertSchema(payload)
    Controller->>SchemaStore: validate templates
    loop each template
        Controller->>Parser: getParser(template.Runtime)
        Parser->>Parser: parse(template.Script)
        alt parse succeeds
            Controller->>SchemaStore: accept template
        else parse fails
            Controller->>API: return InvalidSchema error
            API->>Client: Error
        end
    end
    Controller->>Database: persist schema (with transactions)

    Note over Client,Controller: Create transaction using template
    Client->>API: CreateTransaction(Script.Template="KEY", Vars=...)
    API->>Controller: CreateTransaction(payload)
    Controller->>SchemaStore: FindLatestSchema()
    SchemaStore->>Controller: schema (with transactions)
    Controller->>Controller: resolve template by key
    alt template found
        Controller->>Parser: getParser(template.Runtime)
        Parser->>Parser: parse(template.Script)
        Controller->>Controller: inject template.Script into payload.Plain
        Controller->>Database: Create transaction row (includes Template)
        Database-->>API: created transaction
        API-->>Client: Return transaction (Template set)
    else template missing
        Controller->>API: return InvalidSchema error
        API->>Client: Error
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Review template resolution and error propagation in internal/controller/ledger/controller_default.go
  • Verify type-change impacts (string → *string) across generator, client models, and tests for nil-safety
  • Check DB migration and schema persistence (jsonb transactions, template column) and compatibility
  • Inspect parser/getParser refactor and all call sites for correct ledger.RuntimeType usage
  • Confirm NewSchema template validation wraps and surfaces errors consistently

Poem

🐰 A template tucked in schema's nest,
Parsers hum and runtimes test,
I hop through scripts, inject with care,
Transactions carry keys to share,
Tiny rabbit cheers — templates are here! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: transaction templates' clearly and concisely summarizes the main feature addition, matching the changeset's primary objective of adding transaction template support to ledger schemas.
Description check ✅ Passed The description is directly related to the changeset, explaining the transaction templates feature, schema integration, API changes, and migration guidance, all of which align with the actual changes made.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/transaction_templates

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1212ac9 and 655dbf6.

📒 Files selected for processing (1)
  • internal/storage/ledger/schema_test.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
internal/storage/ledger/schema_test.go (3)
internal/chart.go (1)
  • ChartSegment (21-25)
internal/transaction.go (1)
  • Transactions (15-17)
internal/transaction_templates.go (1)
  • TransactionTemplate (15-19)
🔇 Additional comments (1)
internal/storage/ledger/schema_test.go (1)

22-25: LGTM! Test correctly updated for new schema structure.

The addition of the Transactions field with an empty map is appropriate for this basic schema insertion test. The test continues to validate the core insertion behavior while accommodating the new schema structure introduced in this PR.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Azorlogh Azorlogh force-pushed the feat/transaction_templates branch from 4a8518f to c98d75c Compare December 4, 2025 16:56
@codecov
Copy link

codecov bot commented Dec 4, 2025

Codecov Report

❌ Patch coverage is 58.46154% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.62%. Comparing base (6c8b565) to head (655dbf6).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/controller/ledger/controller_default.go 55.55% 11 Missing and 5 partials ⚠️
internal/schema.go 0.00% 3 Missing and 2 partials ⚠️
internal/api/v2/controllers_transactions_create.go 83.33% 0 Missing and 2 partials ⚠️
internal/errors.go 0.00% 2 Missing ⚠️
internal/transaction_templates.go 60.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1176      +/-   ##
==========================================
- Coverage   76.62%   75.62%   -1.00%     
==========================================
  Files         197      198       +1     
  Lines       10273    10320      +47     
==========================================
- Hits         7872     7805      -67     
- Misses       1445     1458      +13     
- Partials      956     1057     +101     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Azorlogh Azorlogh force-pushed the feat/transaction_templates branch 2 times, most recently from d2a3c75 to 7346fe8 Compare December 8, 2025 19:06
@Azorlogh Azorlogh marked this pull request as ready for review December 8, 2025 19:17
@Azorlogh Azorlogh requested a review from a team as a code owner December 8, 2025 19:17
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 37 files

Prompt for AI agents (all 2 issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="openapi/v2.yaml">

<violation number="1" location="openapi/v2.yaml:2353">
P1: The `script` object still requires `plain` as mandatory, but according to the PR description, clients should be able to use `script.template` + `vars` without providing `plain`. Consider removing the `required: - plain` or using a conditional schema (e.g., `oneOf`) to allow either `plain` or `template` to be provided.</violation>
</file>

<file name="internal/controller/ledger/controller_default.go">

<violation number="1" location="internal/controller/ledger/controller_default.go:479">
P2: Parameter renamed from `_schema` to `schema` but is not used in the function body. Either use the schema parameter (pass it to `store.CommitTransaction` instead of `nil`), or keep the underscore prefix to indicate it&#39;s intentionally unused.</violation>
</file>

Reply to cubic to teach it or ask questions. Re-run a review with @cubic-dev-ai review this PR

$ref: "#/components/schemas/V2AggregatedVolumes"
postCommitEffectiveVolumes:
$ref: "#/components/schemas/V2AggregatedVolumes"
template:
Copy link

@cubic-dev-ai cubic-dev-ai bot Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The script object still requires plain as mandatory, but according to the PR description, clients should be able to use script.template + vars without providing plain. Consider removing the required: - plain or using a conditional schema (e.g., oneOf) to allow either plain or template to be provided.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At openapi/v2.yaml, line 2353:

<comment>The `script` object still requires `plain` as mandatory, but according to the PR description, clients should be able to use `script.template` + `vars` without providing `plain`. Consider removing the `required: - plain` or using a conditional schema (e.g., `oneOf`) to allow either `plain` or `template` to be provided.</comment>

<file context>
@@ -2350,12 +2350,15 @@ components:
           $ref: &quot;#/components/schemas/V2AggregatedVolumes&quot;
         postCommitEffectiveVolumes:
           $ref: &quot;#/components/schemas/V2AggregatedVolumes&quot;
+        template:
+          type: string
       required:
</file context>

✅ Addressed in 137b27b

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove from required: 137b27b

}

func (ctrl *DefaultController) revertTransaction(ctx context.Context, store Store, _schema *ledger.Schema, parameters Parameters[RevertTransaction]) (*ledger.RevertedTransaction, error) {
func (ctrl *DefaultController) revertTransaction(ctx context.Context, store Store, schema *ledger.Schema, parameters Parameters[RevertTransaction]) (*ledger.RevertedTransaction, error) {
Copy link

@cubic-dev-ai cubic-dev-ai bot Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Parameter renamed from _schema to schema but is not used in the function body. Either use the schema parameter (pass it to store.CommitTransaction instead of nil), or keep the underscore prefix to indicate it's intentionally unused.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/controller/ledger/controller_default.go, line 479:

<comment>Parameter renamed from `_schema` to `schema` but is not used in the function body. Either use the schema parameter (pass it to `store.CommitTransaction` instead of `nil`), or keep the underscore prefix to indicate it&#39;s intentionally unused.</comment>

<file context>
@@ -451,7 +476,7 @@ func (ctrl *DefaultController) CreateTransaction(ctx context.Context, parameters
 }
 
-func (ctrl *DefaultController) revertTransaction(ctx context.Context, store Store, _schema *ledger.Schema, parameters Parameters[RevertTransaction]) (*ledger.RevertedTransaction, error) {
+func (ctrl *DefaultController) revertTransaction(ctx context.Context, store Store, schema *ledger.Schema, parameters Parameters[RevertTransaction]) (*ledger.RevertedTransaction, error) {
 	var (
 		hasBeenReverted bool
</file context>

✅ Addressed in 137b27b

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

},
},
},
"template": "",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe omit empty template?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done 👍 8fa193b

m, err := ctrl.getParser(parameters.Input).Parse(parameters.Input.Plain)
if schema != nil {
if parameters.Input.Template == "" {
return nil, newErrSchemaValidationError(parameters.SchemaVersion, fmt.Errorf("transactions on this ledger must use a template"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum, is it a part of the specification?

}
for id, tmpl := range templates {
if tmpl.Runtime == "" {
tmpl.Runtime = RuntimeMachine
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, you should avoid putting logic on UnmarshalXXX functions.
Doing that will force you to implements the same stuff each time you'll implements a new encoding format.
I guess that's something which should be handled at service level.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in: 8fa193b
I just added omitempty + validation step to avoid invalid runtimes

@Azorlogh Azorlogh force-pushed the feat/transaction_templates branch from 137b27b to 7eaef65 Compare December 9, 2025 17:33
@Azorlogh Azorlogh changed the base branch from feat/schema to main December 12, 2025 11:19
@Azorlogh Azorlogh force-pushed the feat/transaction_templates branch from 7eaef65 to 56ee801 Compare December 12, 2025 11:45
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
pkg/generate/generator.go (1)

50-55: Avoid sending plain: "" (use nil when empty) to prevent unintended “script present” validation.
pointer.For(transactionRequest.Script.Plain) will always set a non-nil pointer—even when empty—which can accidentally trip “script is provided” logic (and may conflict with schema/template-only flows).

-					Script: &components.V2PostTransactionScript{
-						Plain: pointer.For(transactionRequest.Script.Plain),
+					Script: &components.V2PostTransactionScript{
+						Plain: func() *string {
+							if transactionRequest.Script.Plain == "" {
+								return nil
+							}
+							return pointer.For(transactionRequest.Script.Plain)
+						}(),
 						Vars: collectionutils.ConvertMap(transactionRequest.Script.Vars, func(from any) string {
 							return fmt.Sprint(from)
 						}),
 					},
internal/errors.go (1)

91-104: Good addition, but ensure callers consistently wrap template/schema validation as ErrInvalidSchema.
NewErrInvalidSchema is helpful; the main risk is some call sites returning raw validation errors instead of ErrInvalidSchema (see internal/schema.go). Optionally guard against nil to avoid invalid schema: <nil> surprises.

internal/storage/bucket/migrations/44-add-schema/up.sql (1)

5-21: Consider DEFAULT '{}'::jsonb for schemas.transactions (NOT NULL + nil map risk).
If any writer inserts schemas without explicitly setting transactions, NOT NULL without a default is fragile. Adding a default keeps the DB resilient even if callers forget to set it.

 		create table schemas (
 			ledger varchar,
 			version text not null,
 			created_at timestamp without time zone not null default now(),
 			chart jsonb not null,
-			transactions jsonb not null,
+			transactions jsonb not null default '{}'::jsonb,
 			primary key (ledger, version)
 		);
internal/api/bulking/elements.go (1)

99-140: Potential validation hole: reject “no postings + no script/template” explicitly.
With the new conditional, an empty request can now reach ledgercontroller.CreateTransaction with a zero-value RunScript (unless downstream rejects it). Safer to fail fast here with a clear error.

 func (req TransactionRequest) ToCore() (*ledgercontroller.CreateTransaction, error) {
 
 	if _, err := req.Postings.Validate(); err != nil {
 		return nil, err
 	}
 
 	var runScript ledgercontroller.RunScript
 	if len(req.Postings) > 0 {
 		txData := ledger.TransactionData{
 			Postings:  req.Postings,
 			Timestamp: req.Timestamp,
 			Reference: req.Reference,
 			Metadata:  req.Metadata,
 		}
 
 		runScript = ledgercontroller.TxToScriptData(txData, req.Force)
 	} else if req.Script.Plain != "" || req.Script.Template != "" {
 		runScript = ledgercontroller.RunScript{
 			Script:    req.Script.ToCore(),
 			Timestamp: req.Timestamp,
 			Reference: req.Reference,
 			Metadata:  req.Metadata,
 		}
+	} else {
+		return nil, fmt.Errorf("invalid transaction request: either postings or script.template/script.plain must be provided")
 	}
 
 	return &ledgercontroller.CreateTransaction{
 		Runtime:         req.Runtime,
 		RunScript:       runScript,
 		AccountMetadata: req.AccountMetadata,
 	}, nil
 }
pkg/client/models/components/v2schemadata.go (1)

6-25: Minor API consistency: consider returning an empty map from GetTransactions() (like GetChart()).
Nil map is safe to range over, but returning {} consistently can reduce nil-check churn for SDK users.

pkg/client/docs/sdks/v2/README.md (1)

1216-1254: Docs bug: example appears to send an invalid combination (schemaVersion + postings + template + plain).
Given the PR behavior, this snippet should demonstrate either postings/plain (no schema template flow) or template+vars (no postings, no plain). Also fix the hard tabs in the code block to satisfy MD010.

         V2PostTransaction: components.V2PostTransaction{
-            Postings: []components.V2Posting{
-                components.V2Posting{
-                    Amount: big.NewInt(100),
-                    Asset: "COIN",
-                    Destination: "users:002",
-                    Source: "users:001",
-                },
-            },
             Script: &components.V2PostTransactionScript{
                 Template: client.String("CUSTOMER_DEPOSIT"),
-                Plain: client.String("vars {\n" +
-                "account $user\n" +
-                "}\n" +
-                "send [COIN 10] (\n" +
-                "	source = @world\n" +
-                "	destination = $user\n" +
-                ")\n" +
-                ""),
                 Vars: map[string]string{
                     "user": "users:042",
                 },
             },

If you want to keep a “plain script” example too, it should omit Template (and, if schema-driven enforcement applies, omit SchemaVersion as well).

docs/api/README.md (1)

726-764: Examples still show “mixed modes” (template + plain and/or postings), which contradicts the documented server rules.
In both Bulk and CreateTransaction examples, script.template is shown alongside script.plain (and the example also includes postings). Per PR objectives, template execution must not be mixed with postings or ad‑hoc scripts, and when a schema is active transactions must use a template. Update examples to show either:

  • Template mode: script: { template, vars } (no plain, no postings), or
  • Legacy mode (no schema): postings or script.plain without script.template.
   "script": {
     "template": "CUSTOMER_DEPOSIT",
-    "plain": "vars {\naccount $user\n}\nsend ...\n",
     "vars": {
       "user": "users:042"
     }
   },

Also applies to: 1580-1612

♻️ Duplicate comments (1)
internal/controller/ledger/controller_default.go (1)

406-433: Schema enforcement: avoid double-reporting + avoid high-cardinality trace attributes

In audit mode, Template == "" currently triggers an error report, then immediately falls into the “template not found” path (another error report), and then proceeds to parse/execute whatever Plain is (possibly empty).

Also attribute.String("schema_validation_failed", err.Error()) risks high-cardinality attributes (template IDs / full error strings).

Suggested shape:

 	if schema != nil {
-		if parameters.Input.Template == "" {
-			err := newErrSchemaValidationError(parameters.SchemaVersion, fmt.Errorf("transactions on this ledger must use a template"))
-			if ctrl.schemaEnforcementMode == SchemaEnforcementStrict {
-				return nil, err
-			} else {
-				trace.SpanFromContext(ctx).SetAttributes(attribute.String("schema_validation_failed", err.Error()))
-				logging.FromContext(ctx).Errorf("schema validation failed: %s", err)
-			}
-		}
-		if template, ok := schema.SchemaData.Transactions[parameters.Input.Template]; ok {
+		if parameters.Input.Template == "" {
+			err := newErrSchemaValidationError(parameters.SchemaVersion, fmt.Errorf("transactions on this ledger must use a template"))
+			if ctrl.schemaEnforcementMode == SchemaEnforcementStrict {
+				return nil, err
+			}
+			trace.SpanFromContext(ctx).SetAttributes(
+				attribute.Bool("schema_validation_failed", true),
+				attribute.String("schema_validation_reason", "missing_template"),
+			)
+			logging.FromContext(ctx).Errorf("schema validation failed: %s", err)
+		} else if template, ok := schema.SchemaData.Transactions[parameters.Input.Template]; ok {
 			parameters.Input.Plain = template.Script
 			if parameters.Input.Runtime == "" {
 				parameters.Input.Runtime = template.Runtime
 			}
 		} else {
 			err := newErrSchemaValidationError(parameters.SchemaVersion, fmt.Errorf("failed to find transaction template `%s`", parameters.Input.Template))
 			if ctrl.schemaEnforcementMode == SchemaEnforcementStrict {
 				return nil, err
 			} else {
-				trace.SpanFromContext(ctx).SetAttributes(attribute.String("schema_validation_failed", err.Error()))
+				trace.SpanFromContext(ctx).SetAttributes(
+					attribute.Bool("schema_validation_failed", true),
+					attribute.String("schema_validation_reason", "unknown_template"),
+				)
 				logging.FromContext(ctx).Errorf("schema validation failed: %s", err)
 			}
 		}
 	}

Also: please confirm the intended spec in audit mode—should ad-hoc scripts still run when a schema is present, or should it be “template-only” regardless? (This is the same concern raised previously.) Based on past review comments, …

🧹 Nitpick comments (13)
internal/transaction.go (1)

37-53: Consider making Transaction.Template nullable (nullzero or *string) to avoid persisting empty-string “unset”.
Not required, but it reduces ambiguity between “no template (legacy)” vs “template id accidentally empty”.

internal/storage/ledger/resource_transactions.go (1)

35-52: Including template in the projection is the right move; consider adding it to the resource schema if filtering is desired.

internal/api/v2/controllers_transactions_create.go (1)

20-35: Multi-type validation is good; improve the error to report all conflicting types (not just first two).
Currently if all three are provided, the message only mentions the first two.

 import (
 	"errors"
 	"fmt"
 	"net/http"
+	"strings"
 ...
 )
 ...
 		if len(txType) > 1 {
-			api.BadRequest(w, common.ErrValidation, fmt.Errorf("cannot pass %v and %v in the same request", txType[0], txType[1]))
+			api.BadRequest(w, common.ErrValidation, fmt.Errorf("cannot mix transaction types in the same request: %s", strings.Join(txType, ", ")))
 			return
 		} else if len(txType) == 0 {
test/e2e/api_transactions_create_test.go (1)

102-111: Pointer migration looks consistent; consider adding at least one schema+template create test.
The pointer.For(...) updates align with Plain becoming nullable. Given the feature, it’d be valuable to add an e2e case that (1) inserts a schema with templates and (2) creates a transaction via script.template (and asserts template info in the resulting transaction/event).

Also applies to: 316-330, 489-501, 519-536, 552-562, 577-593, 602-618, 627-640

pkg/client/models/components/runtime.go (1)

10-35: Nice strict enum parsing; consider adding IsValid() (and optionally MarshalJSON) for reuse.
This can reduce duplicated switch logic elsewhere and make validation callable without JSON roundtrips.

internal/transaction_templates.go (1)

23-29: Avoid per-iteration allocations in TransactionTemplates.Validate()

[]RuntimeType{"", ...} is allocated on each loop iteration; also the error message doesn’t mention that "" is accepted.

 func (t TransactionTemplates) Validate() error {
 	for _, t := range t {
-		if !slices.Contains([]RuntimeType{"", RuntimeMachine, RuntimeExperimentalInterpreter}, t.Runtime) {
-			return fmt.Errorf("unexpected runtime `%s`: should be `%s` or `%s`", t.Runtime, RuntimeMachine, RuntimeExperimentalInterpreter)
-		}
+		switch t.Runtime {
+		case "", RuntimeMachine, RuntimeExperimentalInterpreter:
+			// ok
+		default:
+			return fmt.Errorf(
+				"unexpected runtime %q: should be %q or %q (or omitted)",
+				t.Runtime, RuntimeMachine, RuntimeExperimentalInterpreter,
+			)
+		}
 	}
 	return nil
 }
internal/controller/ledger/controller_default_test.go (2)

26-83: TestCreateTransactionWithoutSchema: consider using a non-empty RunScript.Plain

Right now Parse(runScript.Plain) is parsing "". Even though it’s mocked, using a minimal valid script makes the test intent clearer and reduces “false confidence” if this ever stops being a pure mock.


85-220: TestCreateTransactionWithSchema: make template script + mocked postings consistent

The template script sends EUR/2 while the mocked execution returns a USD posting; since the parser/runtime are mocked, this isn’t functionally wrong, but it’s confusing for future readers.

-	send [EUR/2 100] (
+	send [USD 100] (
 		source = world
 		destination = $dest
 	)
internal/controller/ledger/controller_default.go (4)

64-77: Schema template validation: prefer %q for template IDs (and drop redundant cast)

Minor polish: IDs are user-controlled strings; use %q. Also template.Runtime is already a ledger.RuntimeType.

 	for id, template := range schema.Transactions {
-		parser := ctrl.getParser(ledger.RuntimeType(template.Runtime))
+		parser := ctrl.getParser(template.Runtime)
 		_, err := parser.Parse(template.Script)
 		if err != nil {
-			return nil, ledger.NewErrInvalidSchema(fmt.Errorf("invalid template %s: %w", id, err))
+			return nil, ledger.NewErrInvalidSchema(fmt.Errorf("invalid template %q: %w", id, err))
 		}
 	}

437-446: Small cleanup: inline return m.Execute(...)

No behavior change—just removes a temporary.


481-487: Don’t persist an empty template string

WithTemplate(parameters.Input.Template) is called unconditionally. If empty means “not set”, consider guarding it to avoid storing "" (especially for no-schema flows).

 	transaction := ledger.NewTransaction().
 		WithPostings(result.Postings...).
 		WithMetadata(finalMetadata).
 		WithTimestamp(parameters.Input.Timestamp).
 		WithReference(parameters.Input.Reference).
-		WithTemplate(parameters.Input.Template)
+		WithTemplate(parameters.Input.Template)
+
+	// If `WithTemplate("")` is meaningful today, ignore this; otherwise:
+	// if parameters.Input.Template != "" { transaction = transaction.WithTemplate(parameters.Input.Template) }

573-583: Unused schema parameters: consider _schema to satisfy linters/conventions

Go allows unused parameters, but many linters/code-style conventions prefer _-prefixed names when intentionally unused. (You already do this in revertTransaction.)

Also applies to: 611-626, 633-644

test/e2e/api_schema_test.go (1)

446-470: Pointer construction inconsistency in ad-hoc script test

Elsewhere you use &components.V2PostTransactionScript{...}; here you use pointer.For(components.V2PostTransactionScript{...}). Both work, but mixing styles makes the test harder to scan.

-							Script: pointer.For(components.V2PostTransactionScript{
+							Script: &components.V2PostTransactionScript{
 								Plain: pointer.For(`
 								send [EUR/2] (
 									source = $users:001
 									destination = $bank:001
 								)`),
-							}),
+							},

Also applies to: 472-489

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6c8b565 and 56ee801.

⛔ Files ignored due to path filters (8)
  • docs/events/CommittedTransactions.json is excluded by !**/*.json
  • docs/events/InsertedSchema.json is excluded by !**/*.json
  • docs/events/RevertedTransaction.json is excluded by !**/*.json
  • openapi.yaml is excluded by !**/*.yaml
  • openapi/v2.yaml is excluded by !**/*.yaml
  • pkg/client/.speakeasy/gen.lock is excluded by !**/*.lock, !**/*.lock
  • pkg/client/.speakeasy/logs/naming.log is excluded by !**/*.log, !**/*.log
  • pkg/client/speakeasyusagegen/.speakeasy/logs/naming.log is excluded by !**/*.log, !**/*.log
📒 Files selected for processing (30)
  • docs/api/README.md (32 hunks)
  • internal/README.md (29 hunks)
  • internal/api/bulking/elements.go (2 hunks)
  • internal/api/v2/controllers_transactions_create.go (2 hunks)
  • internal/controller/ledger/controller.go (1 hunks)
  • internal/controller/ledger/controller_default.go (8 hunks)
  • internal/controller/ledger/controller_default_test.go (3 hunks)
  • internal/errors.go (1 hunks)
  • internal/machine/vm/run.go (1 hunks)
  • internal/schema.go (2 hunks)
  • internal/storage/bucket/migrations/44-add-schema/up.sql (1 hunks)
  • internal/storage/ledger/resource_transactions.go (1 hunks)
  • internal/transaction.go (2 hunks)
  • internal/transaction_templates.go (1 hunks)
  • pkg/client/docs/models/components/v2posttransactionscript.md (1 hunks)
  • pkg/client/docs/models/components/v2schema.md (1 hunks)
  • pkg/client/docs/models/components/v2schemadata.md (1 hunks)
  • pkg/client/docs/models/components/v2transaction.md (1 hunks)
  • pkg/client/docs/models/components/v2transactiontemplate.md (1 hunks)
  • pkg/client/docs/sdks/v2/README.md (1 hunks)
  • pkg/client/models/components/runtime.go (1 hunks)
  • pkg/client/models/components/v2posttransaction.go (1 hunks)
  • pkg/client/models/components/v2schema.go (2 hunks)
  • pkg/client/models/components/v2schemadata.go (2 hunks)
  • pkg/client/models/components/v2transaction.go (2 hunks)
  • pkg/client/models/components/v2transactiontemplate.go (1 hunks)
  • pkg/generate/generator.go (1 hunks)
  • test/e2e/api_ledgers_import_test.go (3 hunks)
  • test/e2e/api_schema_test.go (15 hunks)
  • test/e2e/api_transactions_create_test.go (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (9)
pkg/client/models/components/runtime.go (1)
pkg/client/internal/utils/json.go (1)
  • UnmarshalJSON (117-251)
internal/schema.go (3)
internal/chart.go (1)
  • ChartOfAccounts (40-40)
internal/transaction.go (1)
  • Transactions (15-17)
internal/transaction_templates.go (1)
  • TransactionTemplates (21-21)
pkg/generate/generator.go (2)
internal/controller/ledger/controller.go (1)
  • Script (92-92)
internal/api/v1/controllers_transactions_create.go (1)
  • Script (20-23)
pkg/client/models/components/v2transactiontemplate.go (3)
internal/controller/ledger/controller.go (1)
  • Script (92-92)
internal/machine/vm/run.go (1)
  • Script (21-25)
pkg/client/models/components/runtime.go (1)
  • Runtime (11-11)
pkg/client/models/components/v2schemadata.go (2)
internal/transaction.go (1)
  • Transactions (15-17)
pkg/client/models/components/v2transactiontemplate.go (1)
  • V2TransactionTemplate (5-10)
internal/api/bulking/elements.go (5)
pkg/client/models/components/runtime.go (1)
  • Runtime (11-11)
internal/transaction_templates.go (1)
  • RuntimeType (8-8)
internal/controller/ledger/controller.go (1)
  • Script (92-92)
internal/machine/vm/run.go (1)
  • Script (21-25)
internal/api/v1/controllers_transactions_create.go (1)
  • Script (20-23)
internal/controller/ledger/controller.go (2)
pkg/client/models/components/runtime.go (1)
  • Runtime (11-11)
internal/transaction_templates.go (1)
  • RuntimeType (8-8)
internal/api/v2/controllers_transactions_create.go (5)
internal/posting.go (1)
  • Postings (35-35)
internal/controller/ledger/controller.go (1)
  • Script (92-92)
internal/machine/vm/run.go (1)
  • Script (21-25)
internal/api/v1/controllers_transactions_create.go (1)
  • Script (20-23)
internal/api/common/errors.go (2)
  • ErrValidation (20-20)
  • ErrNoPostings (22-22)
internal/controller/ledger/controller_default_test.go (5)
internal/schema.go (2)
  • Schema (16-22)
  • SchemaData (11-14)
internal/machine/vm/run.go (1)
  • Script (21-25)
internal/log.go (3)
  • Log (89-102)
  • InsertedSchemaLogType (25-25)
  • NewTransactionLogType (22-22)
internal/controller/ledger/parameters.go (1)
  • Parameters (3-8)
internal/controller/ledger/numscript_runtime.go (1)
  • NumscriptExecutionResult (18-22)
🪛 markdownlint-cli2 (0.18.1)
pkg/client/docs/models/components/v2posttransactionscript.md

9-9: Hard tabs
Column: 391

(MD010, no-hard-tabs)


9-9: Hard tabs
Column: 412

(MD010, no-hard-tabs)

docs/api/README.md

5798-5798: Multiple headings with the same content

(MD024, no-duplicate-heading)


5827-5827: Multiple headings with the same content

(MD024, no-duplicate-heading)


5860-5860: Multiple headings with the same content

(MD024, no-duplicate-heading)

pkg/client/docs/sdks/v2/README.md

1231-1231: Hard tabs
Column: 18

(MD010, no-hard-tabs)


1232-1232: Hard tabs
Column: 18

(MD010, no-hard-tabs)

🔇 Additional comments (14)
internal/README.md (1)

68-75: Generated public API docs updated appropriately for templates/runtimes.
No concerns on the doc-side changes themselves (assuming gomarkdoc was regenerated from the updated Go types).

Also applies to: 139-174, 165-174, 171-174

pkg/client/models/components/v2transaction.go (1)

11-26: Template field + accessor are consistent and backward compatible.

Also applies to: 129-135

internal/controller/ledger/controller.go (1)

95-100: Runtime now correctly uses the shared ledger.RuntimeType.

internal/transaction.go (1)

93-101: Fluent chaining fix + WithTemplate look good.

pkg/client/docs/models/components/v2transaction.md (1)

20-21: Docs updated to include Template.

pkg/client/docs/models/components/v2schemadata.md (1)

8-11: Docs update matches the new schema surface (Transactions templates).

pkg/client/docs/models/components/v2transactiontemplate.md (1)

1-10: Model docs look aligned; please sanity-check the Runtime/feature-flag wording stays accurate.

pkg/client/models/components/v2schema.go (1)

11-20: LGTM (and consistent with optional transactions,omitempty).

Also applies to: 53-59

pkg/client/docs/models/components/v2schema.md (1)

8-13: Doc accuracy: verify whether Transactions is truly optional at the schema level.
If schemas can be inserted without templates, :heavy_minus_sign: is correct; if at least one template is required when a schema is present, the “Required” column and/or description should reflect that.

pkg/client/models/components/v2transactiontemplate.go (1)

1-31: LGTM for the new SDK model + nil-safe accessors.

test/e2e/api_ledgers_import_test.go (1)

186-231: Test updates for Plain being *string are consistent.

internal/controller/ledger/controller_default.go (1)

391-400: Runtime-based parser selection looks good

test/e2e/api_schema_test.go (2)

57-79: Template-driven schema setup is clear; good reuse across versions

Nice to see one transactionTemplates map reused for v1/v2/v3 schema insertions.

Also applies to: 87-112, 141-142, 171-172


232-252: Good assertion: verify persisted transaction includes Template

Comment on lines 10 to 35
type V2PostTransactionScript struct {
Plain string `json:"plain"`
Vars map[string]string `json:"vars,omitempty"`
Template *string `json:"template,omitempty"`
Plain *string `json:"plain,omitempty"`
Vars map[string]string `json:"vars,omitempty"`
}

func (o *V2PostTransactionScript) GetPlain() string {
func (o *V2PostTransactionScript) GetTemplate() *string {
if o == nil {
return ""
return nil
}
return o.Plain
return o.Template
}

func (o *V2PostTransactionScript) GetVars() map[string]string {
func (o *V2PostTransactionScript) GetPlain() *string {
if o == nil {
return nil
}
return o.Vars
return o.Plain
}

// Runtime - The numscript runtime used to execute the script. Uses "machine" by default, unless the "--experimental-numscript-interpreter" feature flag is passed.
type Runtime string

const (
RuntimeExperimentalInterpreter Runtime = "experimental-interpreter"
RuntimeMachine Runtime = "machine"
)

func (e Runtime) ToPointer() *Runtime {
return &e
}
func (e *Runtime) UnmarshalJSON(data []byte) error {
var v string
if err := json.Unmarshal(data, &v); err != nil {
return err
}
switch v {
case "experimental-interpreter":
fallthrough
case "machine":
*e = Runtime(v)
func (o *V2PostTransactionScript) GetVars() map[string]string {
if o == nil {
return nil
default:
return fmt.Errorf("invalid value for Runtime: %v", v)
}
return o.Vars
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Breaking surface change: Plain is now *string (and Template added)

This changes JSON emission/consumption semantics (empty string vs omitted) and will likely break existing client code that assigns/reads Plain as a value type. Ensure the SDK changelog/migration guidance explicitly calls out:

  • script.plain is now optional/pointer
  • script.template is the preferred path when schemas are used
  • mutual exclusivity expectations (template vs plain) from the API perspective
🤖 Prompt for AI Agents
In pkg/client/models/components/v2posttransaction.go around lines 10 to 35, the
struct change makes Plain a *string and adds Template which alters JSON
emission/consumption (empty string vs omitted) and is a breaking change; update
the SDK release notes and migration guidance to explicitly state that
script.plain is now optional/pointer, script.template is the preferred path when
schemas are used, and document the API expectation that template and plain are
mutually exclusive (include examples of old vs new JSON and recommended
client-side handling), and optionally add helper accessors or constructors that
return non-nil defaults or validation helpers to ease migration for existing
clients.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
internal/machine/vm/run.go (1)

21-25: LGTM! Past review feedback addressed.

The omitempty tags have been correctly added to Plain, Template, and Vars fields as requested in the previous review, ensuring that empty fields are not emitted in JSON and preventing confusion between plain and template script modes.

docs/api/README.md (2)

3985-3985: Clarify template field semantics (unresolved from previous review).

The template field is added to V2Transaction and V2PostTransaction.script but lacks documentation on its presence/absence behavior. Prior feedback (lines 870–872) requested clarification on whether:

  • (a) template is omitted/null when transaction is not from a template, or
  • (b) template is always present (possibly empty string when not from a template)

Update field descriptions (e.g., at line 4007 and 4059) to explicitly state this behavior, and adjust all examples to consistently reflect the chosen semantics (e.g., remove template from non-template transaction examples or set it to null).

Also applies to: 4007-4007, 4059-4059


5784-5865: Address markdownlint MD024 duplicate heading errors (unresolved from previous review).

Static analysis flags duplicate headings at lines 5798, 5827, and 5860 (Runtime, V2TransactionTemplate, V2TransactionTemplates sections). Since this file is auto-generated by Widdershins, you have two options:

  1. Regenerate the spec if duplicate heading definitions exist in the upstream OpenAPI specification.
  2. Disable MD024 locally by wrapping the generated schema block with:
    <!-- markdownlint-disable MD024 -->
    [generated schema sections]
    <!-- markdownlint-enable MD024 -->

If regeneration is not feasible, add disable/enable comments around the schema definitions (before line 5784 and after line 5865) to suppress the linter.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 56ee801 and 1212ac9.

⛔ Files ignored due to path filters (3)
  • openapi.yaml is excluded by !**/*.yaml
  • openapi/v2.yaml is excluded by !**/*.yaml
  • pkg/client/.speakeasy/gen.lock is excluded by !**/*.lock, !**/*.lock
📒 Files selected for processing (10)
  • docs/api/README.md (32 hunks)
  • internal/machine/vm/run.go (1 hunks)
  • internal/schema.go (2 hunks)
  • pkg/client/docs/models/components/v2schema.md (1 hunks)
  • pkg/client/docs/models/components/v2schemadata.md (1 hunks)
  • pkg/client/docs/sdks/v2/README.md (2 hunks)
  • pkg/client/models/components/v2schema.go (2 hunks)
  • pkg/client/models/components/v2schemadata.go (2 hunks)
  • test/e2e/api_idempotency_hit_header_test.go (1 hunks)
  • test/e2e/api_schema_test.go (16 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/client/docs/models/components/v2schemadata.md
🧰 Additional context used
🧬 Code graph analysis (5)
test/e2e/api_idempotency_hit_header_test.go (2)
internal/transaction.go (1)
  • Transactions (15-17)
pkg/client/models/components/v2transactiontemplate.go (1)
  • V2TransactionTemplate (5-10)
internal/schema.go (4)
internal/chart.go (1)
  • ChartOfAccounts (40-40)
internal/transaction.go (1)
  • Transactions (15-17)
internal/transaction_templates.go (1)
  • TransactionTemplates (21-21)
internal/errors.go (1)
  • NewErrInvalidSchema (102-104)
test/e2e/api_schema_test.go (6)
pkg/client/models/components/v2transactiontemplate.go (1)
  • V2TransactionTemplate (5-10)
internal/machine/vm/run.go (1)
  • Script (21-25)
pkg/client/models/components/v2schemadata.go (1)
  • V2SchemaData (6-11)
pkg/client/models/operations/v2createtransaction.go (1)
  • V2CreateTransactionRequest (9-25)
pkg/client/models/components/v2posttransaction.go (2)
  • V2PostTransaction (37-47)
  • V2PostTransactionScript (10-14)
pkg/client/models/components/v2errorsenum.go (2)
  • V2ErrorsEnumNotFound (19-19)
  • V2ErrorsEnumValidation (15-15)
pkg/client/models/components/v2schemadata.go (2)
internal/transaction.go (1)
  • Transactions (15-17)
pkg/client/models/components/v2transactiontemplate.go (1)
  • V2TransactionTemplate (5-10)
pkg/client/models/components/v2schema.go (2)
internal/transaction.go (1)
  • Transactions (15-17)
pkg/client/models/components/v2transactiontemplate.go (1)
  • V2TransactionTemplate (5-10)
🪛 markdownlint-cli2 (0.18.1)
pkg/client/docs/sdks/v2/README.md

1236-1236: Hard tabs
Column: 18

(MD010, no-hard-tabs)


1237-1237: Hard tabs
Column: 18

(MD010, no-hard-tabs)

docs/api/README.md

5798-5798: Multiple headings with the same content

(MD024, no-duplicate-heading)


5827-5827: Multiple headings with the same content

(MD024, no-duplicate-heading)


5860-5860: Multiple headings with the same content

(MD024, no-duplicate-heading)

🔇 Additional comments (13)
test/e2e/api_idempotency_hit_header_test.go (1)

486-486: LGTM! Appropriate initialization of new required field.

The empty Transactions map correctly satisfies the new schema requirement. Since this test focuses on idempotency header behavior rather than template functionality, the empty initialization is appropriate.

pkg/client/docs/models/components/v2schema.md (1)

8-13: LGTM! Documentation accurately reflects the new field.

The Transactions field is properly documented with the correct type and marked as required, consistent with the implementation.

pkg/client/models/components/v2schemadata.go (2)

9-10: LGTM! Consistent field addition.

The Transactions field follows the same pattern as the existing Chart field with appropriate JSON tags and comments.


20-25: LGTM! Nil-safe accessor follows existing pattern.

The GetTransactions() accessor correctly mirrors the GetChart() pattern, returning an empty map when the receiver is nil.

pkg/client/models/components/v2schema.go (2)

18-19: LGTM! Field addition consistent with schema data.

The Transactions field correctly mirrors the structure in V2SchemaData with appropriate documentation.


54-59: LGTM! Nil-safe accessor follows established pattern.

The GetTransactions() accessor is consistent with other getter methods in the struct.

test/e2e/api_schema_test.go (5)

57-79: LGTM! Well-structured template definitions.

The two transaction templates provide good coverage:

  • WORLD_TO_BANK: Simple template with single variable for basic scenarios
  • A_TO_B: More complex template with overdraft allowance for advanced testing

The inline numscript is clear and appropriate for test scenarios.


87-89: Good addition of world account to chart.

Adding world with DotSelf to the chart enables the @world source used in the WORLD_TO_BANK template, ensuring schema validation passes.


247-252: Excellent verification of template field in response.

Fetching the created transaction and asserting the Template field matches expectations ensures the template reference is correctly persisted and returned. This validates a key aspect of the template feature.


428-445: Good coverage of non-existent schema error case.

Testing that a non-existent schema version returns NOT_FOUND ensures proper error handling when clients reference invalid schemas.


447-490: Excellent validation error coverage.

These two tests ensure that when schema enforcement is enabled:

  1. Direct postings are rejected (lines 447-471)
  2. Ad-hoc plain scripts are rejected (lines 473-490)

This validates the core constraint that templates must be used with schema enforcement, preventing clients from bypassing the schema contract.

pkg/client/docs/sdks/v2/README.md (2)

281-286: LGTM! Clear documentation of Transactions field.

The InsertSchema example clearly demonstrates how to include transaction templates in the schema, with a minimal but complete example.


1231-1239: LGTM! Good example of template usage.

The CreateTransaction example clearly shows both Template and Plain fields in the script, demonstrating the dual modes. The example is concise and practical.

Note: The static analysis warning about hard tabs (lines 1236-1237) is a false positive—these are part of the numscript string literal and are intentional for formatting the script.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants