From 818d5a8eca807623882ce97d2c0107f3d23d4070 Mon Sep 17 00:00:00 2001 From: Eric Seidel Date: Sun, 8 Oct 2023 03:00:40 +0000 Subject: [PATCH] feat: support for tracking contract transactions I also renamed Transaction.creditChange to creditsChange This also adds an accounting script (which is how I learned we were missing this information). --- packages/cli/bin/accounting.dart | 72 +++++++++++++ packages/cli/bin/earning_per_ship.dart | 2 +- packages/cli/bin/list_transactions.dart | 2 +- packages/cli/bin/recent_deals.dart | 8 +- packages/cli/lib/behavior/jobs/buy_job.dart | 2 +- packages/cli/lib/behavior/trader.dart | 87 ++++++++++++--- packages/cli/lib/net/actions.dart | 26 ++++- packages/cli/lib/trading.dart | 6 +- packages/db/lib/transaction.dart | 10 +- packages/db/sql/tables/03_transaction.sql | 13 ++- packages/types/lib/contract.dart | 114 ++++++++++++++++++++ packages/types/lib/transaction.dart | 82 ++++++++++++-- packages/types/lib/types.dart | 1 + packages/types/test/deal_test.dart | 4 + packages/types/test/transaction_test.dart | 6 ++ 15 files changed, 392 insertions(+), 43 deletions(-) create mode 100644 packages/cli/bin/accounting.dart create mode 100644 packages/types/lib/contract.dart diff --git a/packages/cli/bin/accounting.dart b/packages/cli/bin/accounting.dart new file mode 100644 index 00000000..dd8178a3 --- /dev/null +++ b/packages/cli/bin/accounting.dart @@ -0,0 +1,72 @@ +import 'package:cli/cli.dart'; +import 'package:cli/printing.dart'; +import 'package:db/transaction.dart'; + +String describeTransaction(Transaction t) { + return '${t.timestamp} ${t.tradeSymbol} ${t.quantity} ${t.tradeType} ' + '${t.shipSymbol} ${t.waypointSymbol} ${t.creditsChange}'; +} + +void reconcile(List transactions) { + final startingCredits = transactions.first.agentCredits; + var credits = startingCredits; + // Skip the first transaction, since agentCredits already includes the + // credits change from that transaction. + for (final t in transactions.skip(1)) { + credits += t.creditsChange; + if (credits != t.agentCredits) { + logger + ..warn('Credits $credits does not match ' + 'agentCredits ${t.agentCredits} ') + ..info(describeTransaction(t)); + credits = t.agentCredits; + } + } +} + +Future command(FileSystem fs, ArgResults argResults) async { + const lookback = Duration(minutes: 60); + final db = await defaultDatabase(); + final startTime = DateTime.timestamp().subtract(lookback); + final transactions = (await transactionsAfter(db, startTime)).toList(); + + final lastCredits = transactions.last.agentCredits; + final firstCredits = transactions.first.agentCredits; + final creditDiff = lastCredits - firstCredits; + final diffSign = creditDiff.isNegative ? '' : '+'; + logger.info( + '$diffSign${creditsString(creditDiff)} ' + 'over ${approximateDuration(lookback)} ' + 'now ${creditsString(lastCredits)}', + ); + final transactionCount = transactions.length; + logger.info('$transactionCount transactions'); + + // Add up the credits change from all transactions. + // The diff should not include the first transaction, since agentCredits + // is the credits *after* that transaction occured. + final computedDiff = + transactions.skip(1).fold(0, (sum, t) => sum + t.creditsChange); + if (computedDiff != creditDiff) { + logger.warn( + 'Computed diff $computedDiff does not match ' + 'actual diff $creditDiff', + ); + reconcile(transactions); + } + // Print the counts by transaction type. + final counts = {}; + for (final t in transactions) { + counts[t.accounting] = (counts[t.accounting] ?? 0) + 1; + } + for (final type in AccountingType.values) { + final count = counts[type] ?? 0; + logger.info('$count $type'); + } + + await db.close(); +} + +void main(List args) { + runOffline(args, command); +} diff --git a/packages/cli/bin/earning_per_ship.dart b/packages/cli/bin/earning_per_ship.dart index 0b8e1283..27fcef74 100644 --- a/packages/cli/bin/earning_per_ship.dart +++ b/packages/cli/bin/earning_per_ship.dart @@ -15,7 +15,7 @@ class TransactionSummary { bool get isEmpty => transactions.isEmpty; - int get creditDiff => transactions.fold(0, (m, t) => m + t.creditChange); + int get creditDiff => transactions.fold(0, (m, t) => m + t.creditsChange); Duration get duration => transactions.isEmpty ? Duration.zero : transactions.last.timestamp.difference(transactions.first.timestamp); diff --git a/packages/cli/bin/list_transactions.dart b/packages/cli/bin/list_transactions.dart index 4c7b27a0..1a52e537 100644 --- a/packages/cli/bin/list_transactions.dart +++ b/packages/cli/bin/list_transactions.dart @@ -3,7 +3,7 @@ import 'package:db/transaction.dart'; String describeTransaction(Transaction t) { return '${t.timestamp} ${t.tradeSymbol} ${t.quantity} ${t.tradeType} ' - '${t.shipSymbol} ${t.waypointSymbol} ${t.creditChange} ${t.accounting}'; + '${t.shipSymbol} ${t.waypointSymbol} ${t.creditsChange} ${t.accounting}'; } Future command(FileSystem fs, ArgResults argResults) async { diff --git a/packages/cli/bin/recent_deals.dart b/packages/cli/bin/recent_deals.dart index 6880c43c..c2a071bf 100644 --- a/packages/cli/bin/recent_deals.dart +++ b/packages/cli/bin/recent_deals.dart @@ -9,7 +9,7 @@ void main(List args) async { String describeTransaction(Transaction t) { return '${t.timestamp} ${t.tradeSymbol} ${t.quantity} ${t.tradeType} ' - '${t.shipSymbol} ${t.waypointSymbol} ${t.creditChange}'; + '${t.shipSymbol} ${t.waypointSymbol} ${t.creditsChange}'; } class SyntheticDeal { @@ -46,11 +46,11 @@ class SyntheticDeal { int get units => goodsBuys.fold(0, (sum, t) => sum + t.quantity); int get costOfGoodsSold => - goodsBuys.fold(0, (sum, t) => sum + t.creditChange); + goodsBuys.fold(0, (sum, t) => sum + t.creditsChange); int get revenue => transactions .where((t) => t.tradeType == MarketTransactionTypeEnum.SELL) - .fold(0, (sum, t) => sum + t.creditChange); + .fold(0, (sum, t) => sum + t.creditsChange); int get operatingExpenses => transactions .where( @@ -58,7 +58,7 @@ class SyntheticDeal { t.tradeType == MarketTransactionTypeEnum.PURCHASE && t.accounting == AccountingType.fuel, ) - .fold(0, (sum, t) => sum + t.creditChange); + .fold(0, (sum, t) => sum + t.creditsChange); int get profit => revenue + costOfGoodsSold + operatingExpenses; diff --git a/packages/cli/lib/behavior/jobs/buy_job.dart b/packages/cli/lib/behavior/jobs/buy_job.dart index 4834b24f..052158aa 100644 --- a/packages/cli/lib/behavior/jobs/buy_job.dart +++ b/packages/cli/lib/behavior/jobs/buy_job.dart @@ -147,7 +147,7 @@ Future doBuyJob( ship, 'Purchased ${transaction.quantity} ${transaction.tradeSymbol} ' '@ ${transaction.perUnitPrice} ' - '${creditsString(transaction.creditChange)}', + '${creditsString(transaction.creditsChange)}', ); } jobAssert( diff --git a/packages/cli/lib/behavior/trader.dart b/packages/cli/lib/behavior/trader.dart index a74c43ce..83df54ee 100644 --- a/packages/cli/lib/behavior/trader.dart +++ b/packages/cli/lib/behavior/trader.dart @@ -149,7 +149,7 @@ Future _handleAtSourceWithDeal( 'Purchased ${transaction.quantity} ${transaction.tradeSymbol} ' '@ ${transaction.perUnitPrice} (expected ' '${creditsString(nextExpectedPrice)}) = ' - '${creditsString(transaction.creditChange)}', + '${creditsString(transaction.creditsChange)}', ); } } @@ -240,6 +240,7 @@ Future _handleArbitrageDealAtDestination( /// Handle contract deal at destination. Future _handleContractDealAtDestination( Api api, + Database db, CentralCommand centralCommand, Caches caches, Ship ship, @@ -252,6 +253,8 @@ Future _handleContractDealAtDestination( final neededGood = contract!.goodNeeded(costedDeal.tradeSymbol); final maybeResponse = await _deliverContractGoodsIfPossible( api, + db, + caches.agent, caches.contracts, caches.ships, ship, @@ -263,24 +266,51 @@ Future _handleContractDealAtDestination( // decide next loop if we need to do more. state.isComplete = true; - if (maybeResponse != null) { - // Update our cargo counts after fulfilling the contract. - ship.cargo = maybeResponse.cargo; - // If we've delivered enough, complete the contract. - if (maybeResponse.contract.goodNeeded(contractGood)!.amountNeeded <= 0) { - final response = await api.contracts.fulfillContract(contract.id); - final data = response!.data; - caches.agent.agent = data.agent; - caches.contracts.updateContract(data.contract); - shipInfo(ship, 'Contract complete!'); - return null; - } + // If we've delivered enough, complete the contract. + if (maybeResponse != null && + maybeResponse.contract.goodNeeded(contractGood)!.amountNeeded <= 0) { + await _completeContract( + api, + db, + caches, + ship, + maybeResponse.contract, + ); + return null; } return null; } +Future _completeContract( + Api api, + Database db, + Caches caches, + Ship ship, + Contract contract, +) async { + final response = await api.contracts.fulfillContract(contract.id); + final data = response!.data; + caches.agent.agent = data.agent; + caches.contracts.updateContract(data.contract); + + final contactTransaction = ContractTransaction.fulfillment( + contract: contract, + shipSymbol: ship.shipSymbol, + waypointSymbol: ship.waypointSymbol, + timestamp: DateTime.timestamp(), + ); + final transaction = Transaction.fromContractTransaction( + contactTransaction, + caches.agent.agent.credits, + ); + await db.insertTransaction(transaction); + shipInfo(ship, 'Contract complete!'); +} + Future _deliverContractGoodsIfPossible( Api api, + Database db, + AgentCache agentCache, ContractCache contractCache, ShipCache shipCache, Ship ship, @@ -301,6 +331,7 @@ Future _deliverContractGoodsIfPossible( return null; } + final unitsBefore = ship.countUnits(goods.tradeSymbolObject); // And we have the desired cargo. final response = await deliverContract( api, @@ -319,6 +350,24 @@ Future _deliverContractGoodsIfPossible( '${deliver.unitsFulfilled}/${deliver.unitsRequired}, ' '${approximateDuration(contract.timeUntilDeadline)} to deadline', ); + + // Update our cargo counts after delivering the contract goods. + final unitsAfter = ship.countUnits(goods.tradeSymbolObject); + final unitsDelivered = unitsAfter - unitsBefore; + + // Record the delivery transaction. + final contactTransaction = ContractTransaction.delivery( + contract: contract, + shipSymbol: ship.shipSymbol, + waypointSymbol: ship.waypointSymbol, + unitsDelivered: unitsDelivered, + timestamp: DateTime.timestamp(), + ); + final transaction = Transaction.fromContractTransaction( + contactTransaction, + agentCache.agent.credits, + ); + await db.insertTransaction(transaction); return response; } @@ -388,6 +437,7 @@ Future _handleAtDestinationWithDeal( if (costedDeal.isContractDeal) { return _handleContractDealAtDestination( api, + db, centralCommand, caches, ship, @@ -500,6 +550,7 @@ String describeExpectedContractProfit( /// Accepts contracts for us if needed. Future acceptContractsIfNeeded( Api api, + Database db, ContractCache contractCache, MarketPrices marketPrices, AgentCache agentCache, @@ -515,7 +566,14 @@ Future acceptContractsIfNeeded( return null; } for (final contract in contractCache.unacceptedContracts) { - await acceptContractAndLog(api, contractCache, agentCache, contract); + await acceptContractAndLog( + api, + db, + contractCache, + agentCache, + ship, + contract, + ); } return null; } @@ -689,6 +747,7 @@ Future advanceTrader( if (centralCommand.isContractTradingEnabled) { await acceptContractsIfNeeded( api, + db, caches.contracts, caches.marketPrices, caches.agent, diff --git a/packages/cli/lib/net/actions.dart b/packages/cli/lib/net/actions.dart index 1da288d8..f9a4742c 100644 --- a/packages/cli/lib/net/actions.dart +++ b/packages/cli/lib/net/actions.dart @@ -216,6 +216,7 @@ Future purchaseShipAndLog( shipErr(ship, 'Bought ship: ${result.ship.symbol}'); final transaction = Transaction.fromShipyardTransaction( result.transaction, + // purchaseShip updated the agentCache agentCache.agent.credits, ship.shipSymbol, ); @@ -456,19 +457,34 @@ Future negotiateContractAndLog( /// Accept [contract] and log. Future acceptContractAndLog( Api api, + Database db, ContractCache contractCache, AgentCache agentCache, + Ship ship, Contract contract, ) async { final response = await api.contracts.acceptContract(contract.id); final data = response!.data; agentCache.agent = data.agent; contractCache.updateContract(data.contract); - logger - ..info('Accepted: ${contractDescription(contract)}.') - ..info( - 'received ${creditsString(contract.terms.payment.onAccepted)}', - ); + shipInfo(ship, 'Accepted: ${contractDescription(contract)}.'); + shipInfo( + ship, + 'received ${creditsString(contract.terms.payment.onAccepted)}', + ); + + final contactTransaction = ContractTransaction.accept( + contract: contract, + shipSymbol: ship.shipSymbol, + waypointSymbol: ship.waypointSymbol, + timestamp: DateTime.timestamp(), + ); + final transaction = Transaction.fromContractTransaction( + contactTransaction, + agentCache.agent.credits, + ); + await db.insertTransaction(transaction); + return data; } diff --git a/packages/cli/lib/trading.dart b/packages/cli/lib/trading.dart index 61f665b6..853fdb28 100644 --- a/packages/cli/lib/trading.dart +++ b/packages/cli/lib/trading.dart @@ -240,7 +240,7 @@ extension CostedDealPrediction on CostedDeal { int get actualRevenue { return transactions .where((t) => t.tradeType == MarketTransactionTypeEnum.SELL) - .fold(0, (a, b) => a + b.creditChange); + .fold(0, (a, b) => a + b.creditsChange); } /// The actual cost of goods sold. @@ -248,7 +248,7 @@ extension CostedDealPrediction on CostedDeal { return transactions .where((t) => t.tradeType == MarketTransactionTypeEnum.PURCHASE) .where((t) => t.accounting == AccountingType.goods) - .fold(0, (a, b) => a + -b.creditChange); + .fold(0, (a, b) => a + -b.creditsChange); } /// The actual operational expenses of the deal. @@ -256,7 +256,7 @@ extension CostedDealPrediction on CostedDeal { return transactions .where((t) => t.tradeType == MarketTransactionTypeEnum.PURCHASE) .where((t) => t.accounting == AccountingType.fuel) - .fold(0, (a, b) => a + -b.creditChange); + .fold(0, (a, b) => a + -b.creditsChange); } /// The actual profit of the deal. diff --git a/packages/db/lib/transaction.dart b/packages/db/lib/transaction.dart index 42e47edb..6ab0d55e 100644 --- a/packages/db/lib/transaction.dart +++ b/packages/db/lib/transaction.dart @@ -23,16 +23,19 @@ Map transactionToColumnMap(Transaction transaction) { 'trade_symbol': transaction.tradeSymbol?.toJson(), 'ship_type': transaction.shipType?.toJson(), 'quantity': transaction.quantity, - 'trade_type': transaction.tradeType.value, + 'trade_type': transaction.tradeType?.value, 'per_unit_price': transaction.perUnitPrice, 'timestamp': transaction.timestamp, 'agent_credits': transaction.agentCredits, 'accounting': transaction.accounting.name, + 'contract_action': transaction.contractAction?.name, + 'contract_id': transaction.contractId, }; } /// Create a new transaction from a result row. Transaction transactionFromColumnMap(Map values) { + final contractAction = values['contract_action'] as String?; return Transaction( transactionType: TransactionType.fromName(values['transaction_type'] as String), @@ -43,11 +46,14 @@ Transaction transactionFromColumnMap(Map values) { shipType: ShipType.fromJson(values['ship_type'] as String?), quantity: values['quantity'] as int, tradeType: - MarketTransactionTypeEnum.fromJson(values['trade_type'] as String)!, + MarketTransactionTypeEnum.fromJson(values['trade_type'] as String), perUnitPrice: values['per_unit_price'] as int, timestamp: values['timestamp'] as DateTime, agentCredits: values['agent_credits'] as int, accounting: AccountingType.fromName(values['accounting'] as String), + contractAction: + contractAction == null ? null : ContractAction.fromName(contractAction), + contractId: values['contract_id'] as String?, ); } diff --git a/packages/db/sql/tables/03_transaction.sql b/packages/db/sql/tables/03_transaction.sql index 0990a48d..a2c20744 100644 --- a/packages/db/sql/tables/03_transaction.sql +++ b/packages/db/sql/tables/03_transaction.sql @@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS "transaction_" ( -- The waypoint symbol where the transaction was made. "waypoint_symbol" text NOT NULL, -- The trade symbol of the transaction. - -- Trade symbol is null for shipyard transactions. + -- Trade symbol is null for non-trade transactions. "trade_symbol" text, -- The ship type of the transaction. -- Ship type is null for non-shipyard transactions. @@ -17,7 +17,8 @@ CREATE TABLE IF NOT EXISTS "transaction_" ( -- The quantity of units transacted. "quantity" int NOT NULL, -- The trade type of the transaction. - "trade_type" text NOT NULL, + -- The trade type is null for non-trade transactions. + "trade_type" text, -- The per-unit price of the transaction. "per_unit_price" int NOT NULL, -- The timestamp of the transaction. @@ -25,5 +26,11 @@ CREATE TABLE IF NOT EXISTS "transaction_" ( -- The credits of the agent after the transaction. "agent_credits" int NOT NULL, -- The accounting classification of the transaction. - "accounting" text NOT NULL + "accounting" text NOT NULL, + -- The contract id of the transaction. + -- Contract id is null for non-contract transactions. + "contract_id" text, + -- The contract action of the transaction. + -- Contract action is null for non-contract transactions. + "contract_action" text, ); diff --git a/packages/types/lib/contract.dart b/packages/types/lib/contract.dart new file mode 100644 index 00000000..b14409b0 --- /dev/null +++ b/packages/types/lib/contract.dart @@ -0,0 +1,114 @@ +import 'package:meta/meta.dart'; +import 'package:types/api.dart'; + +/// The type of contract action. +enum ContractAction { + /// Accept a contract. + accept, + + /// Deliver goods for a contract. + delivery, + + /// Fulfill a contract. + fulfillment; + + /// Failed to fulfill a contract before the deadline. Advance is reclaimed. + /// We get no notice from the server, so to implement this we'd need to + /// account for it on *every* transaction. This is not worth the effort. + // failure; + + /// Lookup a contract action by name. + static ContractAction fromName(String name) { + return ContractAction.values.firstWhere((e) => e.name == name); + } +} + +/// A class to hold transaction data from a contract. +@immutable +class ContractTransaction { + const ContractTransaction._({ + required this.contractId, + required this.contractAction, + required this.unitsDelivered, + required this.creditsChange, + required this.shipSymbol, + required this.timestamp, + required this.waypointSymbol, + }); + + /// Accept a contract. + factory ContractTransaction.accept({ + required Contract contract, + required ShipSymbol shipSymbol, + required WaypointSymbol waypointSymbol, + required DateTime timestamp, + }) { + return ContractTransaction._( + contractId: contract.id, + contractAction: ContractAction.accept, + unitsDelivered: null, + creditsChange: contract.terms.payment.onAccepted, + shipSymbol: shipSymbol, + timestamp: timestamp, + waypointSymbol: waypointSymbol, + ); + } + + /// Deliver goods for a contract. + factory ContractTransaction.delivery({ + required Contract contract, + required int unitsDelivered, + required ShipSymbol shipSymbol, + required WaypointSymbol waypointSymbol, + required DateTime timestamp, + }) { + return ContractTransaction._( + contractId: contract.id, + contractAction: ContractAction.delivery, + unitsDelivered: unitsDelivered, + creditsChange: 0, + shipSymbol: shipSymbol, + timestamp: timestamp, + waypointSymbol: waypointSymbol, + ); + } + + /// Fulfill a contract. + factory ContractTransaction.fulfillment({ + required Contract contract, + required ShipSymbol shipSymbol, + required WaypointSymbol waypointSymbol, + required DateTime timestamp, + }) { + return ContractTransaction._( + contractId: contract.id, + contractAction: ContractAction.fulfillment, + unitsDelivered: null, + creditsChange: contract.terms.payment.onFulfilled, + shipSymbol: shipSymbol, + timestamp: timestamp, + waypointSymbol: waypointSymbol, + ); + } + + /// The ID of the contract. + final String contractId; + + /// The type of contract action. + final ContractAction contractAction; + + /// The number of units delivered. + final int? unitsDelivered; + + /// The change in credits. + final int creditsChange; + + /// The ShipSymbol of the ship that performed the action. + final ShipSymbol shipSymbol; + + /// The timestamp of the action. + final DateTime timestamp; + + /// The location of the action. + final WaypointSymbol waypointSymbol; +} diff --git a/packages/types/lib/transaction.dart b/packages/types/lib/transaction.dart index 9a2f0d91..66853a6d 100644 --- a/packages/types/lib/transaction.dart +++ b/packages/types/lib/transaction.dart @@ -1,5 +1,6 @@ import 'package:meta/meta.dart'; import 'package:types/api.dart'; +import 'package:types/contract.dart'; /// The accounting type of a transaction. enum AccountingType { @@ -18,16 +19,30 @@ enum AccountingType { } } +// Saf uses: +// TRADEGOOD_TRANSACTION_TYPE_CHOICES = ( +// (-3, 'ship modification'), +// (-2, 'ship purchase'), +// (-1, 'purchase'), +// (0, 'unknown'), +// (1, 'sell'), +// (2, 'contract delivery'), +// ) +// https://discord.com/channels/792864705139048469/792864705139048472/1157736812819787776 + /// The type of transaction which created this transaction. enum TransactionType { - /// A market transaction. + /// A market transaction (buy or sell goods or refuel). market, - /// A shipyard transaction. + /// A shipyard transaction (ship purchase). shipyard, - /// A ship modification transaction. - shipModification; + /// A ship modification transaction (mount). + shipModification, + + /// A contract transaction + contract; /// Lookup a transaction type by index. static TransactionType fromName(String name) { @@ -51,9 +66,13 @@ class Transaction { required this.timestamp, required this.agentCredits, required this.accounting, + required this.contractId, + required this.contractAction, }); /// Create a new transaction to allow any() use in mocks. + /// Can also be used for round-trip tests. Uses all fields, but is not + /// a valid transaction. @visibleForTesting Transaction.fallbackValue() : this( @@ -61,13 +80,15 @@ class Transaction { shipSymbol: const ShipSymbol('A', 1), waypointSymbol: WaypointSymbol.fromString('S-E-P'), tradeSymbol: TradeSymbol.FUEL, - shipType: null, + shipType: ShipType.EXPLORER, quantity: 1, tradeType: MarketTransactionTypeEnum.PURCHASE, perUnitPrice: 2, timestamp: DateTime(2021), agentCredits: 3, accounting: AccountingType.goods, + contractId: 'abcd', + contractAction: ContractAction.delivery, ); /// Create a new transaction from json. @@ -88,6 +109,9 @@ class Transaction { agentCredits: json['agentCredits'] as int, accounting: AccountingType.values .firstWhere((e) => e.name == json['accounting'] as String), + contractId: json['contractId'] as String?, + contractAction: ContractAction.values + .firstWhere((e) => e.name == json['contractAction'] as String?), ); } @@ -111,6 +135,8 @@ class Transaction { timestamp: transaction.timestamp, agentCredits: agentCredits, accounting: accounting, + contractId: null, + contractAction: null, ); } @@ -138,6 +164,8 @@ class Transaction { timestamp: transaction.timestamp, agentCredits: agentCredits, accounting: AccountingType.capital, + contractId: null, + contractAction: null, ); } @@ -166,6 +194,30 @@ class Transaction { timestamp: transaction.timestamp, agentCredits: agentCredits, accounting: AccountingType.capital, + contractId: null, + contractAction: null, + ); + } + + /// Create a new transaction from a contract transaction. + factory Transaction.fromContractTransaction( + ContractTransaction transaction, + int agentCredits, + ) { + return Transaction( + transactionType: TransactionType.contract, + shipSymbol: transaction.shipSymbol, + waypointSymbol: transaction.waypointSymbol, + tradeSymbol: null, + shipType: null, + quantity: transaction.unitsDelivered ?? 0, + tradeType: MarketTransactionTypeEnum.PURCHASE, + perUnitPrice: 0, + timestamp: DateTime.now(), + agentCredits: agentCredits, + accounting: AccountingType.capital, + contractId: transaction.contractId, + contractAction: transaction.contractAction, ); } @@ -188,7 +240,7 @@ class Transaction { final int quantity; /// Market transaction type (e.g. PURCHASE, SELL) - final MarketTransactionTypeEnum tradeType; + final MarketTransactionTypeEnum? tradeType; /// Per-unit price of the transaction. final int perUnitPrice; @@ -202,8 +254,14 @@ class Transaction { /// The accounting classification of the transaction. final AccountingType accounting; + /// The id of the contract involved in the transaction. + final String? contractId; + + /// The action of the contract involved in the transaction. + final ContractAction? contractAction; + /// The change in credits from this transaction. - int get creditChange { + int get creditsChange { if (tradeType == MarketTransactionTypeEnum.PURCHASE) { return -perUnitPrice * quantity; } else { @@ -221,11 +279,13 @@ class Transaction { 'tradeSymbol': tradeSymbol?.toJson(), 'shipType': shipType?.toJson(), 'quantity': quantity, - 'tradeType': tradeType.value, + 'tradeType': tradeType?.value, 'perUnitPrice': perUnitPrice, 'timestamp': timestamp.toUtc().toIso8601String(), 'agentCredits': agentCredits, 'accounting': accounting.name, + 'contractId': contractId, + 'contractAction': contractAction?.name, }; } @@ -244,7 +304,9 @@ class Transaction { perUnitPrice == other.perUnitPrice && timestamp == other.timestamp && agentCredits == other.agentCredits && - accounting == other.accounting; + accounting == other.accounting && + contractId == other.contractId && + contractAction == other.contractAction; @override int get hashCode => Object.hash( @@ -259,5 +321,7 @@ class Transaction { timestamp, agentCredits, accounting, + contractId, + contractAction, ); } diff --git a/packages/types/lib/types.dart b/packages/types/lib/types.dart index 8c92ffee..ed9e02a9 100644 --- a/packages/types/lib/types.dart +++ b/packages/types/lib/types.dart @@ -2,6 +2,7 @@ export 'package:openapi/api.dart'; export 'api.dart'; export 'behavior.dart'; +export 'contract.dart'; export 'deal.dart'; export 'enum.dart'; export 'extraction.dart'; diff --git a/packages/types/test/deal_test.dart b/packages/types/test/deal_test.dart index 7b14c63b..8ae9edde 100644 --- a/packages/types/test/deal_test.dart +++ b/packages/types/test/deal_test.dart @@ -116,6 +116,8 @@ void main() { timestamp: DateTime(2021), agentCredits: 10, accounting: AccountingType.fuel, + contractAction: null, + contractId: null, ); final transaction2 = Transaction( transactionType: TransactionType.market, @@ -129,6 +131,8 @@ void main() { timestamp: DateTime(2021), agentCredits: 10, accounting: AccountingType.fuel, + contractAction: null, + contractId: null, ); final costed2 = costed.byAddingTransactions([transaction1]); expect(costed2.transactions, [transaction1]); diff --git a/packages/types/test/transaction_test.dart b/packages/types/test/transaction_test.dart index 3cd47ddb..74557bf3 100644 --- a/packages/types/test/transaction_test.dart +++ b/packages/types/test/transaction_test.dart @@ -16,6 +16,8 @@ void main() { timestamp: moonLanding, agentCredits: 3, accounting: AccountingType.capital, + contractAction: ContractAction.accept, + contractId: '1234', ); final json = transaction.toJson(); final transaction2 = Transaction.fromJson(json); @@ -43,6 +45,8 @@ void main() { timestamp: moonLanding, agentCredits: 3, accounting: AccountingType.capital, + contractAction: null, + contractId: null, ); final transaction2 = Transaction( transactionType: TransactionType.market, @@ -56,6 +60,8 @@ void main() { timestamp: moonLanding, agentCredits: 1, accounting: AccountingType.capital, + contractAction: null, + contractId: null, ); // Note: Quanity and agentCredits are swapped relative to transaction1: expect(transaction.hashCode, isNot(transaction2.hashCode));