mirror of
https://github.com/corda/corda.git
synced 2025-02-21 09:51:57 +00:00
Remove experimental trade finance code: it's not maintained, hasn't been code reviewed and we don't wish to release it as open source.
This commit is contained in:
parent
3c09056e22
commit
d1b279c2b2
@ -1,165 +0,0 @@
|
||||
package net.corda.contracts
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.PublicKeyTree
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
import java.util.*
|
||||
|
||||
val ACCOUNTRECEIVABLE_PROGRAM_ID = AccountReceivable()
|
||||
|
||||
/*
|
||||
|
||||
|
||||
*/
|
||||
|
||||
|
||||
|
||||
class AccountReceivable : Contract {
|
||||
|
||||
enum class StatusEnum {
|
||||
Applied,
|
||||
Issued
|
||||
}
|
||||
|
||||
data class AccountReceivableProperties(
|
||||
val invoiceID: String,
|
||||
val exporter: LocDataStructures.Company,
|
||||
val buyer: LocDataStructures.Company,
|
||||
val currency: Issued<Currency>,
|
||||
val invoiceDate: LocalDate,
|
||||
val invoiceAmount: Amount<Issued<Currency>>,
|
||||
val purchaseDate: LocalDate,
|
||||
val maturityDate: LocalDate,
|
||||
val discountRate: Double // should be a number between 0 and 1.0. 90% = 0.9
|
||||
|
||||
) {
|
||||
|
||||
val purchaseAmount: Amount<Issued<Currency>> = invoiceAmount.times((discountRate * 100).toInt()).div(100)
|
||||
}
|
||||
|
||||
data class State(
|
||||
// technical variables
|
||||
override val owner: PublicKeyTree,
|
||||
val status: StatusEnum,
|
||||
val props: AccountReceivableProperties
|
||||
|
||||
) : OwnableState {
|
||||
override val contract = ACCOUNTRECEIVABLE_PROGRAM_ID
|
||||
|
||||
override val participants: List<PublicKeyTree>
|
||||
get() = listOf(owner)
|
||||
|
||||
override fun toString() = "AR owned by $owner)"
|
||||
|
||||
fun checkInvoice(invoice: Invoice.State): Boolean {
|
||||
val arProps = Helper.invoicePropsToARProps(invoice.props, props.discountRate)
|
||||
return props == arProps
|
||||
}
|
||||
|
||||
override fun withNewOwner(newOwner: PublicKeyTree) = Pair(Commands.Issue(), copy(owner = newOwner, status = StatusEnum.Issued))
|
||||
|
||||
}
|
||||
|
||||
companion object Helper {
|
||||
fun invoicePropsToARProps(invoiceProps: Invoice.InvoiceProperties, discountRate: Double): AccountReceivableProperties {
|
||||
return AccountReceivable.AccountReceivableProperties(
|
||||
invoiceProps.invoiceID,
|
||||
invoiceProps.seller, invoiceProps.buyer, invoiceProps.goodCurrency,
|
||||
invoiceProps.invoiceDate, invoiceProps.amount, LocalDate.MIN,
|
||||
invoiceProps.payDate, discountRate)
|
||||
}
|
||||
|
||||
fun createARFromInvoice(invoice: Invoice.State, discountRate: Double, notary: Party): TransactionState<AccountReceivable.State> {
|
||||
val arProps = invoicePropsToARProps(invoice.props, discountRate)
|
||||
val ar = AccountReceivable.State(invoice.owner.owningKey, StatusEnum.Applied, arProps)
|
||||
return TransactionState<AccountReceivable.State>(ar, notary)
|
||||
}
|
||||
|
||||
fun generateAR(invoice: StateAndRef<Invoice.State>, discountRate: Double, notary: Party): TransactionBuilder {
|
||||
if (invoice.state.data.assigned) {
|
||||
throw IllegalArgumentException("Cannot build AR with an already assigned invoice")
|
||||
}
|
||||
val ar = createARFromInvoice(invoice.state.data, discountRate, notary)
|
||||
val tx = TransactionType.General.Builder(notary)
|
||||
tx.addInputState(invoice)
|
||||
tx.addOutputState(invoice.state.data.copy(assigned = true))
|
||||
tx.addCommand(Invoice.Commands.Assign(), invoice.state.data.owner.owningKey)
|
||||
tx.addOutputState(ar)
|
||||
tx.addCommand(AccountReceivable.Commands.Apply(), invoice.state.data.owner.owningKey)
|
||||
return tx
|
||||
}
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
// Seller offer AR to bank
|
||||
class Apply : TypeOnlyCommandData(), Commands
|
||||
|
||||
// Bank check the paper, and accept or reject
|
||||
class Issue : TypeOnlyCommandData(), Commands
|
||||
|
||||
// When buyer paid to bank, case close
|
||||
class Extinguish : TypeOnlyCommandData(), Commands
|
||||
}
|
||||
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
val command = tx.commands.requireSingleCommand<AccountReceivable.Commands>()
|
||||
|
||||
val time = tx.timestamp?.midpoint ?:
|
||||
throw IllegalArgumentException("must be timestamped")
|
||||
|
||||
when (command.value) {
|
||||
is AccountReceivable.Commands.Apply -> {
|
||||
val invoiceCommand = tx.commands.requireSingleCommand<Invoice.Commands>()
|
||||
if (invoiceCommand.value !is Invoice.Commands.Assign) {
|
||||
throw IllegalArgumentException("The invoice command associated must be 'Assign'")
|
||||
}
|
||||
|
||||
if (tx.inputs.size != 1) {
|
||||
throw IllegalArgumentException("There must be an input Invoice state")
|
||||
}
|
||||
val inputInvoice: Invoice.State = tx.inputs.filterIsInstance<Invoice.State>().single()
|
||||
if (tx.outputs.size != 2) {
|
||||
throw IllegalArgumentException("There must be two output states")
|
||||
}
|
||||
val newAR: AccountReceivable.State = tx.outputs.filterIsInstance<AccountReceivable.State>().single()
|
||||
|
||||
requireThat {
|
||||
"AR state must be applied" by (newAR.status == StatusEnum.Applied)
|
||||
"AR properties must match input invoice" by newAR.checkInvoice(inputInvoice)
|
||||
"The discount factor is invalid" by (newAR.props.discountRate >= 0.0 &&
|
||||
newAR.props.discountRate <= 1.0)
|
||||
"the payment date must be in the the future" by
|
||||
(newAR.props.maturityDate.atStartOfDay().toInstant(ZoneOffset.UTC) >= time)
|
||||
}
|
||||
}
|
||||
is AccountReceivable.Commands.Issue -> {
|
||||
val oldAR: AccountReceivable.State = tx.inputs.filterIsInstance<AccountReceivable.State>().single()
|
||||
val newAR: AccountReceivable.State = tx.outputs.filterIsInstance<AccountReceivable.State>().single()
|
||||
|
||||
requireThat {
|
||||
"input status must be applied" by (oldAR.status == StatusEnum.Applied)
|
||||
"output status must be issued" by (newAR.status == StatusEnum.Issued)
|
||||
"properties must match" by (newAR.props == oldAR.props)
|
||||
}
|
||||
}
|
||||
is AccountReceivable.Commands.Extinguish -> {
|
||||
val oldAR: AccountReceivable.State = tx.inputs.filterIsInstance<AccountReceivable.State>().single()
|
||||
val newAR: AccountReceivable.State? = tx.outputs.filterIsInstance<AccountReceivable.State>().singleOrNull()
|
||||
|
||||
requireThat {
|
||||
"input status must be issued" by (oldAR.status == StatusEnum.Issued)
|
||||
"output state must not exist" by (newAR == null)
|
||||
"the payment date must be today or in the the past" by
|
||||
(oldAR.props.maturityDate.atStartOfDay().toInstant(ZoneOffset.UTC) <= time)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// legal Prose
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("AccountReceivable")
|
||||
}
|
@ -1,142 +0,0 @@
|
||||
package net.corda.contracts
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.PublicKeyTree
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.days
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Bill of Lading Agreement
|
||||
//
|
||||
|
||||
val BILL_OF_LADING_PROGRAM_ID = BillOfLadingAgreement()
|
||||
|
||||
/**
|
||||
* A bill of lading is a standard-form document. It is transferable by endorsement (or by lawful transfer of possession)
|
||||
* and is a receipt from shipping company regarding the number of packages with a particular weight and markings and a
|
||||
* contract for the transportation of same to a port of destination mentioned therein.
|
||||
*
|
||||
* An order bill of lading is used when shipping merchandise prior to payment, requiring a carrier to deliver the
|
||||
* merchandise to the importer, and at the endorsement of the exporter the carrier may transfer title to the importer.
|
||||
* Endorsed order bills of lading can be traded as a security or serve as collateral against debt obligations.
|
||||
*/
|
||||
|
||||
|
||||
class BillOfLadingAgreement : Contract {
|
||||
|
||||
data class BillOfLadingProperties(
|
||||
val billOfLadingID: String,
|
||||
val issueDate: LocalDate,
|
||||
val carrierOwner: Party,
|
||||
val nameOfVessel: String,
|
||||
val descriptionOfGoods: List<LocDataStructures.Good>,
|
||||
val portOfLoading: LocDataStructures.Port,
|
||||
val portOfDischarge: LocDataStructures.Port,
|
||||
val grossWeight: LocDataStructures.Weight,
|
||||
val dateOfShipment: LocalDate?,
|
||||
val shipper: LocDataStructures.Company?,
|
||||
val notify: LocDataStructures.Person?,
|
||||
val consignee: LocDataStructures.Company?
|
||||
) {}
|
||||
|
||||
data class State(
|
||||
// technical variables
|
||||
override val owner: PublicKeyTree,
|
||||
val beneficiary: Party,
|
||||
val props: BillOfLadingProperties
|
||||
|
||||
) : OwnableState {
|
||||
override val participants: List<PublicKeyTree>
|
||||
get() = listOf(owner)
|
||||
|
||||
override fun withNewOwner(newOwner: PublicKeyTree): Pair<CommandData, OwnableState> {
|
||||
return Pair(Commands.TransferPossession(), copy(owner = newOwner))
|
||||
}
|
||||
|
||||
override val contract = BILL_OF_LADING_PROGRAM_ID
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
class IssueBL : TypeOnlyCommandData(), Commands
|
||||
class TransferAndEndorseBL : TypeOnlyCommandData(), Commands
|
||||
class TransferPossession : TypeOnlyCommandData(), Commands
|
||||
}
|
||||
|
||||
/** The Invoice contract needs to handle three commands
|
||||
* 1: IssueBL --
|
||||
* 2: TransferAndEndorseBL --
|
||||
* 3: TransferPossession --
|
||||
*/
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
val command = tx.commands.requireSingleCommand<BillOfLadingAgreement.Commands>()
|
||||
|
||||
val time = tx.timestamp?.midpoint
|
||||
if (time == null) throw IllegalArgumentException("must be timestamped")
|
||||
|
||||
val txOutputStates: List<BillOfLadingAgreement.State> = tx.outputs.filterIsInstance<BillOfLadingAgreement.State>()
|
||||
val txInputStates: List<BillOfLadingAgreement.State> = tx.inputs.filterIsInstance<BillOfLadingAgreement.State>()
|
||||
|
||||
when (command.value) {
|
||||
|
||||
is Commands.IssueBL -> {
|
||||
requireThat {
|
||||
"there is no input state" by txInputStates.isEmpty()
|
||||
"the transaction is signed by the carrier" by (command.signers.contains(txOutputStates.single().props.carrierOwner.owningKey))
|
||||
}
|
||||
}
|
||||
is Commands.TransferAndEndorseBL -> {
|
||||
requireThat {
|
||||
"the transaction is signed by the beneficiary" by (command.signers.contains(txInputStates.single().beneficiary.owningKey))
|
||||
"the transaction is signed by the state object owner" by (command.signers.contains(txInputStates.single().owner))
|
||||
"the bill of lading agreement properties are unchanged" by (txInputStates.single().props == txOutputStates.single().props)
|
||||
}
|
||||
}
|
||||
is Commands.TransferPossession -> {
|
||||
requireThat {
|
||||
"the transaction is signed by the state object owner" by (command.signers.contains(txInputStates.single().owner))
|
||||
//"the state object owner has been updated" by (txInputStates.single().owner != txOutputStates.single().owner)
|
||||
"the beneficiary is unchanged" by (txInputStates.single().beneficiary == txOutputStates.single().beneficiary)
|
||||
"the bill of lading agreement properties are unchanged" by (txInputStates.single().props == txOutputStates.single().props)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Bill_of_lading")
|
||||
|
||||
/**
|
||||
* Returns a transaction that issues a Bill of Lading Agreement
|
||||
*/
|
||||
fun generateIssue(owner: PublicKeyTree, beneficiary: Party, props: BillOfLadingProperties, notary: Party): TransactionBuilder {
|
||||
val state = State(owner, beneficiary, props)
|
||||
val builder = TransactionType.General.Builder(notary = notary)
|
||||
builder.setTime(Instant.now(), 1.days)
|
||||
return builder.withItems(state, Command(Commands.IssueBL(), props.carrierOwner.owningKey))
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the given partial transaction with an input/output/command to reassign ownership of the paper.
|
||||
*/
|
||||
fun generateTransferAndEndorse(tx: TransactionBuilder, BoL: StateAndRef<State>, newOwner: PublicKeyTree, newBeneficiary: Party) {
|
||||
tx.addInputState(BoL)
|
||||
tx.addOutputState(BoL.state.data.copy(owner = newOwner, beneficiary = newBeneficiary))
|
||||
val signers: List<PublicKeyTree> = listOf(BoL.state.data.owner, BoL.state.data.beneficiary.owningKey)
|
||||
tx.addCommand(Commands.TransferAndEndorseBL(), signers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the given partial transaction with an input/output/command to reassign ownership of the paper.
|
||||
*/
|
||||
fun generateTransferPossession(tx: TransactionBuilder, BoL: StateAndRef<State>, newOwner: PublicKeyTree) {
|
||||
tx.addInputState(BoL)
|
||||
tx.addOutputState(BoL.state.data.copy(owner = newOwner))
|
||||
// tx.addOutputState(BoL.state.data.copy().withNewOwner(newOwner))
|
||||
tx.addCommand(Commands.TransferPossession(), BoL.state.data.owner)
|
||||
}
|
||||
|
||||
}
|
@ -1,167 +0,0 @@
|
||||
package net.corda.contracts
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.PublicKeyTree
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
import java.util.*
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Invoice
|
||||
//
|
||||
|
||||
val INVOICE_PROGRAM_ID = Invoice()
|
||||
|
||||
// TODO: Any custom exceptions needed?
|
||||
|
||||
/**
|
||||
* An invoice is a document that describes a trade between a buyer and a seller. It is issued on a particular date,
|
||||
* it lists goods being sold by the seller, the cost of each good and the total amount owed by the buyer and when
|
||||
* the buyer expects to be paid by.
|
||||
*
|
||||
* In the trade finance world, invoices are used to create other contracts (for example AccountsReceivable), newly
|
||||
* created invoices start off with a status of "unassigned", once they're used to create other contracts the status
|
||||
* is changed to "assigned". This ensures that an invoice is used only once when creating a financial product like
|
||||
* AccountsReceivable.
|
||||
*
|
||||
*/
|
||||
|
||||
class Invoice : Contract {
|
||||
|
||||
|
||||
data class InvoiceProperties(
|
||||
val invoiceID: String,
|
||||
val seller: LocDataStructures.Company,
|
||||
val buyer: LocDataStructures.Company,
|
||||
val invoiceDate: LocalDate,
|
||||
val term: Long,
|
||||
val goods: List<LocDataStructures.PricedGood> = ArrayList()
|
||||
) {
|
||||
init {
|
||||
require(term > 0) { "the term must be a positive number" }
|
||||
require(goods.isNotEmpty()) { "there must be goods assigned to the invoice" }
|
||||
}
|
||||
|
||||
// returns the single currency used by the goods list
|
||||
val goodCurrency: Issued<Currency> get() = goods.map { it.unitPrice.token }.distinct().single()
|
||||
|
||||
// iterate over the goods list and sum up the price for each
|
||||
val amount: Amount<Issued<Currency>> get() = goods.map { it.totalPrice() }.sumOrZero(goodCurrency)
|
||||
|
||||
// add term to invoice date to determine the payDate
|
||||
val payDate: LocalDate get() {
|
||||
return invoiceDate.plusDays(term)
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class State(
|
||||
// technical variables
|
||||
val owner: Party,
|
||||
val buyer: Party,
|
||||
val assigned: Boolean,
|
||||
val props: InvoiceProperties,
|
||||
override val linearId: UniqueIdentifier = UniqueIdentifier()
|
||||
) : LinearState {
|
||||
|
||||
override val contract = INVOICE_PROGRAM_ID
|
||||
|
||||
override val participants: List<PublicKeyTree>
|
||||
get() = listOf(owner.owningKey)
|
||||
|
||||
// returns true when the actual business properties of the
|
||||
// invoice is modified
|
||||
fun propertiesChanged(otherState: State): Boolean {
|
||||
return (props != otherState.props)
|
||||
}
|
||||
|
||||
fun generateInvoice(notary: Party? = null): TransactionBuilder = Invoice().generateInvoice(props, owner, buyer, notary)
|
||||
|
||||
// iterate over the goods list and sum up the price for each
|
||||
val amount: Amount<Issued<Currency>> get() = props.amount
|
||||
|
||||
override fun isRelevant(ourKeys: Set<PublicKey>): Boolean {
|
||||
return owner.owningKey.containsAny(ourKeys) || buyer.owningKey.containsAny(ourKeys)
|
||||
}
|
||||
}
|
||||
|
||||
fun generateInvoice(props: InvoiceProperties, owner: Party, buyer: Party, notary: Party? = null): TransactionBuilder {
|
||||
val state = State(owner, buyer, false, props)
|
||||
return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Issue(), listOf(owner.owningKey)))
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Issue : TypeOnlyCommandData(), Commands
|
||||
class Assign : TypeOnlyCommandData(), Commands
|
||||
class Extinguish : TypeOnlyCommandData(), Commands
|
||||
}
|
||||
|
||||
/** The Invoice contract needs to handle three commands
|
||||
* 1: Issue -- the creation of the Invoice contract. We need to confirm that the correct
|
||||
* party signed the contract and that the relevant fields are populated with valid data.
|
||||
* 2: Assign -- the invoice is used to create another type of Contract. The assigned boolean has to change from
|
||||
* false to true.
|
||||
* 3: Extinguish -- the invoice is deleted. Proper signing is required.
|
||||
*
|
||||
*/
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
val command = tx.commands.requireSingleCommand<Invoice.Commands>()
|
||||
|
||||
val time = tx.timestamp?.midpoint ?:
|
||||
throw IllegalArgumentException("must be timestamped")
|
||||
|
||||
when (command.value) {
|
||||
is Commands.Issue -> {
|
||||
if (tx.outputs.size != 1) {
|
||||
throw IllegalArgumentException("Failed requirement: during issuance of the invoice, only " +
|
||||
"one output invoice state should be include in the transaction. " +
|
||||
"Number of output states included was " + tx.outputs.size)
|
||||
}
|
||||
val issueOutput: Invoice.State = tx.outputs.filterIsInstance<Invoice.State>().single()
|
||||
|
||||
requireThat {
|
||||
"there is no input state" by tx.inputs.filterIsInstance<State>().isEmpty()
|
||||
"the transaction is signed by the invoice owner" by (command.signers.contains(issueOutput.owner.owningKey))
|
||||
"the buyer and seller must be different" by (issueOutput.props.buyer.name != issueOutput.props.seller.name)
|
||||
"the invoice must not be assigned" by (issueOutput.assigned == false)
|
||||
"the invoice ID must not be blank" by (issueOutput.props.invoiceID.length > 0)
|
||||
"the term must be a positive number" by (issueOutput.props.term > 0)
|
||||
"the payment date must be in the future" by (issueOutput.props.payDate.atStartOfDay().toInstant(ZoneOffset.UTC) > time)
|
||||
"there must be goods associated with the invoice" by (issueOutput.props.goods.isNotEmpty())
|
||||
"the invoice amount must be non-zero" by (issueOutput.amount.quantity > 0)
|
||||
}
|
||||
}
|
||||
is Commands.Assign -> {
|
||||
val assignInput: Invoice.State = tx.inputs.filterIsInstance<Invoice.State>().single()
|
||||
val assignOutput: Invoice.State = tx.outputs.filterIsInstance<Invoice.State>().single()
|
||||
|
||||
requireThat {
|
||||
"input state owner must be the same as the output state owner" by (assignInput.owner == assignOutput.owner)
|
||||
"the transaction must be signed by the owner" by (command.signers.contains(assignInput.owner.owningKey))
|
||||
"the invoice properties must remain unchanged" by (!assignOutput.propertiesChanged(assignInput))
|
||||
"the input invoice must not be assigned" by (assignInput.assigned == false)
|
||||
"the output invoice must be assigned" by (assignOutput.assigned == true)
|
||||
"the payment date must be in the future" by (assignInput.props.payDate.atStartOfDay().toInstant(ZoneOffset.UTC) > time)
|
||||
}
|
||||
}
|
||||
is Commands.Extinguish -> {
|
||||
val extinguishInput: Invoice.State = tx.inputs.filterIsInstance<Invoice.State>().single()
|
||||
val extinguishOutput: Invoice.State? = tx.outputs.filterIsInstance<Invoice.State>().singleOrNull()
|
||||
|
||||
requireThat {
|
||||
"there shouldn't be an output state" by (extinguishOutput == null)
|
||||
"the transaction must be signed by the owner" by (command.signers.contains(extinguishInput.owner.owningKey))
|
||||
// "the payment date must be today or in the past" by (extinguishInput.props.payDate.atStartOfDay().toInstant(ZoneOffset.UTC) < time)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("Invoice")
|
||||
}
|
@ -1,150 +0,0 @@
|
||||
package net.corda.contracts
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.NullPublicKeyTree
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.PublicKeyTree
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.time.LocalDate
|
||||
import java.time.Period
|
||||
import java.util.*
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Letter of Credit Application
|
||||
//
|
||||
|
||||
// Just a fake program identifier for now. In a real system it could be, for instance, the hash of the program bytecode.
|
||||
val LC_APPLICATION_PROGRAM_ID = LCApplication()
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class LCApplication : Contract {
|
||||
// TODO: should reference the content of the legal agreement, not its URI
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("Letter of Credit Application")
|
||||
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
val command = tx.commands.requireSingleCommand<LCApplication.Commands>()
|
||||
val inputs = tx.inputs.filterIsInstance<State>()
|
||||
val outputs = tx.outputs.filterIsInstance<State>()
|
||||
|
||||
when (command.value) {
|
||||
is Commands.ApplyForLC -> {
|
||||
verifyApply(inputs, outputs, command as AuthenticatedObject<Commands.ApplyForLC>, tx)
|
||||
}
|
||||
is Commands.Approve -> {
|
||||
verifyApprove(inputs, outputs, command as AuthenticatedObject<Commands.Approve>, tx)
|
||||
}
|
||||
|
||||
// TODO: Think about how to evolve contracts over time with new commands.
|
||||
else -> throw IllegalArgumentException("Unrecognised command")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyApply(inputs: List<State>, outputs: List<State>, command: AuthenticatedObject<Commands.ApplyForLC>, tx: TransactionForContract) {
|
||||
val output = outputs.single()
|
||||
val applicant = output.props.applicant
|
||||
|
||||
requireThat {
|
||||
//TODO - Is this required???
|
||||
"the owner must be the issuer" by output.props.issuer.owningKey.equals(output.owner)
|
||||
"there is no input state" by inputs.isEmpty()
|
||||
"the transaction is signed by the applicant" by (command.signers.contains(applicant.owningKey))
|
||||
//TODO - sales contract attached
|
||||
//TODO - purchase order attached
|
||||
//TODO - application confirms to required template
|
||||
"the state is propagated" by (outputs.size == 1)
|
||||
"the output status must be pending issuer review" by (output.status.equals(Status.PENDING_ISSUER_REVIEW))
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyApprove(inputs: List<State>, outputs: List<State>, command: AuthenticatedObject<Commands.Approve>, tx: TransactionForContract) {
|
||||
val input = inputs.single()
|
||||
val output = outputs.single()
|
||||
val issuer = output.owner
|
||||
|
||||
requireThat {
|
||||
//TODO - signed by owner
|
||||
"the transaction is signed by the issuer bank (object owner)" by (command.signers.contains(issuer))
|
||||
"the input status must be pending issuer review" by (input.status.equals(Status.PENDING_ISSUER_REVIEW))
|
||||
"the output status must be approved" by (output.status.equals(Status.APPROVED))
|
||||
}
|
||||
}
|
||||
|
||||
enum class Status {
|
||||
PENDING_ISSUER_REVIEW,
|
||||
APPROVED,
|
||||
REJECTED
|
||||
}
|
||||
|
||||
data class LCApplicationProperties(
|
||||
val letterOfCreditApplicationID: String,
|
||||
val applicationDate: LocalDate,
|
||||
val typeCredit: LocDataStructures.CreditType,
|
||||
val issuer: Party,
|
||||
val beneficiary: Party,
|
||||
val applicant: Party,
|
||||
val expiryDate: LocalDate,
|
||||
val portLoading: LocDataStructures.Port,
|
||||
val portDischarge: LocDataStructures.Port,
|
||||
val placePresentation: LocDataStructures.Location,
|
||||
val lastShipmentDate: LocalDate,
|
||||
val periodPresentation: Period,
|
||||
val goods: List<LocDataStructures.PricedGood> = ArrayList(),
|
||||
val documentsRequired: List<String> = ArrayList(),
|
||||
val invoiceRef: StateRef,
|
||||
val amount: Amount<Issued<Currency>>
|
||||
) {
|
||||
init {
|
||||
if (periodPresentation == null || periodPresentation.isZero) {
|
||||
// TODO: set default value???
|
||||
// periodPresentation = Period.ofDays(21)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class State(
|
||||
val owner: PublicKeyTree,
|
||||
val status: Status,
|
||||
val props: LCApplicationProperties
|
||||
) : ContractState {
|
||||
|
||||
override val contract = LC_APPLICATION_PROGRAM_ID
|
||||
|
||||
override val participants: List<PublicKeyTree>
|
||||
get() = listOf(owner)
|
||||
|
||||
// returns true when the actual business properties of the
|
||||
// invoice is modified
|
||||
fun propertiesChanged(otherState: State): Boolean {
|
||||
return (props != otherState.props)
|
||||
}
|
||||
|
||||
// iterate over the goods list and sum up the price for each
|
||||
fun withoutOwner() = copy(owner = NullPublicKeyTree)
|
||||
}
|
||||
|
||||
fun generateApply(props: LCApplicationProperties, notary: Party, purchaseOrder: Attachment): TransactionBuilder {
|
||||
val state = State(props.issuer.owningKey, Status.PENDING_ISSUER_REVIEW, props)
|
||||
val txBuilder = TransactionType.General.Builder(notary).withItems(state, Command(Commands.ApplyForLC(), props.applicant.owningKey))
|
||||
txBuilder.addAttachment(purchaseOrder.id)
|
||||
return txBuilder
|
||||
}
|
||||
|
||||
fun generateApprove(tx: TransactionBuilder, application: StateAndRef<LCApplication.State>) {
|
||||
tx.addInputState(application)
|
||||
tx.addOutputState(application.state.data.copy(status = Status.APPROVED))
|
||||
tx.addCommand(Commands.Approve(), application.state.data.owner)
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
class ApplyForLC : TypeOnlyCommandData(), Commands
|
||||
class Approve : TypeOnlyCommandData(), Commands
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,154 +0,0 @@
|
||||
package net.corda.contracts
|
||||
|
||||
import net.corda.contracts.asset.sumCashBy
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.PublicKeyTree
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.days
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.Period
|
||||
import java.time.ZoneOffset
|
||||
import java.util.*
|
||||
|
||||
val LOC_PROGRAM_ID = LOC()
|
||||
|
||||
/** LOC contract - consists of the following commands 1. Issue 2. DemandPresentation 3. Termination ***/
|
||||
|
||||
|
||||
class LOC : Contract {
|
||||
|
||||
|
||||
data class Company(
|
||||
val name: String,
|
||||
val address: String,
|
||||
val phone: String?
|
||||
)
|
||||
|
||||
data class LOCProperties(
|
||||
val letterOfCreditID: String,
|
||||
val applicationDate: LocalDate,
|
||||
val issueDate: LocalDate,
|
||||
val typeCredit: LocDataStructures.CreditType,
|
||||
val amount: Amount<Issued<Currency>>,
|
||||
val invoiceRef: StateRef,
|
||||
val expiryDate: LocalDate,
|
||||
val portLoading: LocDataStructures.Port,
|
||||
val portDischarge: LocDataStructures.Port,
|
||||
val descriptionGoods: List<LocDataStructures.PricedGood>,
|
||||
val placePresentation: LocDataStructures.Location,
|
||||
val latestShip: LocalDate,
|
||||
val periodPresentation: Period,
|
||||
val beneficiary: Party,
|
||||
val issuingbank: Party,
|
||||
val appplicant: Party
|
||||
|
||||
) {
|
||||
}
|
||||
|
||||
data class State(
|
||||
// technical variables
|
||||
val beneficiaryPaid: Boolean,
|
||||
val issued: Boolean,
|
||||
val terminated: Boolean,
|
||||
val props: LOC.LOCProperties
|
||||
|
||||
) : ContractState {
|
||||
override val contract = LOC_PROGRAM_ID
|
||||
|
||||
override val participants: List<PublicKeyTree>
|
||||
get() = listOf()
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Issuance : TypeOnlyCommandData(), Commands
|
||||
class DemandPresentation : TypeOnlyCommandData(), Commands
|
||||
class Termination : TypeOnlyCommandData(), Commands
|
||||
}
|
||||
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
val command = tx.commands.requireSingleCommand<LOC.Commands>()
|
||||
|
||||
val time = tx.timestamp?.midpoint
|
||||
if (time == null) throw IllegalArgumentException("must be timestamped")
|
||||
|
||||
when (command.value) {
|
||||
is Commands.Issuance -> {
|
||||
|
||||
// val LOCappInput: LOCapp.State = tx.inStates.filterIsInstance<LOCapp.State>().single()
|
||||
val LOCissueOutput: LOC.State = tx.outputs.filterIsInstance<LOC.State>().single()
|
||||
|
||||
requireThat {
|
||||
// "there is no input state" by !tx.inStates.filterIsInstance<State>().isEmpty() TODO: verify if LOC application is submitted
|
||||
//"LOC application has not been submitted" by (tx.inStates.filterIsInstance<LOCapp.State>().count() == 1)
|
||||
"the transaction is not signed by the issuing bank" by (command.signers.contains(LOCissueOutput.props.issuingbank.owningKey))
|
||||
"the LOC must be Issued" by (LOCissueOutput.issued == true)
|
||||
"Demand Presentation must not be preformed successfully" by (LOCissueOutput.beneficiaryPaid == false)
|
||||
"LOC must not be terminated" by (LOCissueOutput.terminated == false)
|
||||
"the period of presentation must be a positive number" by (!LOCissueOutput.props.periodPresentation.isNegative && !LOCissueOutput.props.periodPresentation.isZero)
|
||||
}
|
||||
}
|
||||
is Commands.DemandPresentation -> {
|
||||
|
||||
|
||||
val LOCInput: LOC.State = tx.inputs.filterIsInstance<LOC.State>().single()
|
||||
val invoiceInput: Invoice.State = tx.inputs.filterIsInstance<Invoice.State>().single()
|
||||
|
||||
val LOCdemandOutput: LOC.State = tx.outputs.filterIsInstance<LOC.State>().single()
|
||||
val BOLtransferOutput: BillOfLadingAgreement.State = tx.outputs.filterIsInstance<BillOfLadingAgreement.State>().single()
|
||||
|
||||
val CashpayOutput = tx.outputs.sumCashBy(LOCdemandOutput.props.beneficiary.owningKey)
|
||||
|
||||
requireThat {
|
||||
|
||||
"there is no input state" by !tx.inputs.filterIsInstance<State>().isEmpty()
|
||||
"the transaction is signed by the issuing bank" by (command.signers.contains(LOCdemandOutput.props.issuingbank.owningKey))
|
||||
"the transaction is signed by the Beneficiary" by (command.signers.contains(LOCdemandOutput.props.beneficiary.owningKey))
|
||||
"the LOC properties do not remain the same" by (LOCInput.props.equals(LOCdemandOutput.props))
|
||||
"the LOC expiry date has passed" by (LOCdemandOutput.props.expiryDate.atStartOfDay().toInstant(ZoneOffset.UTC) > time)
|
||||
"the shipment is late" by (LOCdemandOutput.props.latestShip > (BOLtransferOutput.props.dateOfShipment ?: BOLtransferOutput.props.issueDate))
|
||||
"the cash state has not been transferred" by (CashpayOutput.token.equals(invoiceInput.amount.token) && CashpayOutput.quantity >= (invoiceInput.amount.quantity))
|
||||
"the bill of lading has not been transferred" by (LOCdemandOutput.props.appplicant.owningKey.equals(BOLtransferOutput.beneficiary.owningKey))
|
||||
"the beneficiary has not been paid, status not changed" by (LOCdemandOutput.beneficiaryPaid == true)
|
||||
"the LOC must be Issued" by (LOCdemandOutput.issued == true)
|
||||
"LOC must not be terminated" by (LOCdemandOutput.terminated == false)
|
||||
// "the presentation is late" by (time <= (LOCdemandOutput.props.periodPresentation.addTo(LOCdemandOutput.props.issueDate) as LocalDate).atStartOfDay().toInstant(ZoneOffset.UTC) )
|
||||
|
||||
}
|
||||
}
|
||||
is Commands.Termination -> {
|
||||
|
||||
val LOCterminateOutput: LOC.State = tx.outputs.filterIsInstance<LOC.State>().single()
|
||||
//val CashpayOutput2: Cash.State = tx.outputs.filterIsInstance<Cash.State>().single()
|
||||
val CashpayOutput = tx.outputs.sumCashBy(LOCterminateOutput.props.issuingbank.owningKey)
|
||||
|
||||
val LOCinput: LOC.State = tx.inputs.filterIsInstance<LOC.State>().single()
|
||||
|
||||
requireThat {
|
||||
"the transaction is signed by the issuing bank" by (command.signers.contains(LOCterminateOutput.props.issuingbank.owningKey))
|
||||
//"the transaction is signed by the applicant" by (command.signers.contains(LOCterminateOutput.props.appplicant.owningKey))
|
||||
"the cash state has not been transferred" by (CashpayOutput.token.equals(LOCterminateOutput.props.amount.token) && CashpayOutput.quantity >= (LOCterminateOutput.props.amount.quantity))
|
||||
"the beneficiary has not been paid, status not changed" by (LOCterminateOutput.beneficiaryPaid == true)
|
||||
"the LOC must be Issued" by (LOCterminateOutput.issued == true)
|
||||
"LOC should be terminated" by (LOCterminateOutput.terminated == true)
|
||||
"the LOC properties do not remain the same" by (LOCinput.props.equals(LOCterminateOutput.props))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("LOC")
|
||||
|
||||
fun generateIssue(beneficiaryPaid: Boolean, issued: Boolean, terminated: Boolean, props: LOCProperties, notary: Party): TransactionBuilder {
|
||||
val state = State(beneficiaryPaid, issued, terminated, props)
|
||||
val builder = TransactionType.General.Builder(notary = notary)
|
||||
builder.setTime(Instant.now(), 1.days)
|
||||
return builder.withItems(state, Command(Commands.Issuance(), props.issuingbank.owningKey))
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
package net.corda.contracts
|
||||
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.Issued
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Created by N992551 on 30.06.2016.
|
||||
*/
|
||||
|
||||
object LocDataStructures {
|
||||
enum class WeightUnit {
|
||||
KG,
|
||||
LBS
|
||||
}
|
||||
|
||||
data class Weight(
|
||||
val quantity: Double,
|
||||
val unit: LocDataStructures.WeightUnit
|
||||
)
|
||||
|
||||
data class Company(
|
||||
val name: String,
|
||||
val address: String,
|
||||
val phone: String?
|
||||
)
|
||||
|
||||
data class Person(
|
||||
val name: String,
|
||||
val address: String,
|
||||
val phone: String?
|
||||
)
|
||||
|
||||
data class Port(
|
||||
val country: String,
|
||||
val city: String,
|
||||
val address: String?,
|
||||
val name: String?,
|
||||
val state: String?
|
||||
)
|
||||
|
||||
data class Location(
|
||||
val country: String,
|
||||
val state: String?,
|
||||
val city: String
|
||||
)
|
||||
|
||||
data class Good(
|
||||
val description: String,
|
||||
val quantity: Int,
|
||||
val grossWeight: LocDataStructures.Weight?
|
||||
) {
|
||||
init {
|
||||
require(quantity > 0) { "The good quantity must be a positive value." }
|
||||
}
|
||||
}
|
||||
|
||||
data class PricedGood(
|
||||
val description: String,
|
||||
val purchaseOrderRef: String?,
|
||||
val quantity: Int,
|
||||
val unitPrice: Amount<Issued<Currency>>,
|
||||
val grossWeight: LocDataStructures.Weight?
|
||||
) {
|
||||
init {
|
||||
require(quantity > 0) { "The good quantity must be a positive value." }
|
||||
}
|
||||
|
||||
fun totalPrice(): Amount<Issued<Currency>> {
|
||||
return unitPrice.times(quantity)
|
||||
}
|
||||
}
|
||||
|
||||
enum class CreditType {
|
||||
//TODO: There are a lot of types
|
||||
SIGHT,
|
||||
DEFERRED_PAYMENT,
|
||||
ACCEPTANCE,
|
||||
NEGOTIABLE_CREDIT,
|
||||
TRANSFERABLE,
|
||||
STANDBY,
|
||||
REVOLVING,
|
||||
RED_CLAUSE,
|
||||
GREEN_CLAUSE
|
||||
}
|
||||
|
||||
}
|
@ -1,340 +0,0 @@
|
||||
package net.corda.contracts
|
||||
|
||||
import net.corda.core.contracts.DOLLARS
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.contracts.`issued by`
|
||||
import net.corda.core.seconds
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
import net.corda.core.utilities.TEST_TX_TIME
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.*
|
||||
import org.junit.Before
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
|
||||
/**
|
||||
* unit test cases that confirms the correct behavior of the AccountReceivable smart contract
|
||||
*/
|
||||
|
||||
|
||||
// TODO: Fix all tests to use new DSL
|
||||
|
||||
class AccountReceivableTests {
|
||||
val INVOICE_TIME = Instant.parse("2015-04-17T12:00:00.00Z")
|
||||
val PAST_INVOICE_TIME = Instant.parse("2014-04-17T12:00:00.00Z")
|
||||
|
||||
val defaultRef = OpaqueBytes(ByteArray(1, { 1 }))
|
||||
val defaultIssuer = MEGA_CORP.ref(defaultRef)
|
||||
|
||||
val notary = DUMMY_NOTARY
|
||||
|
||||
lateinit var services: MockServices
|
||||
|
||||
val invoiceProperties = Invoice.InvoiceProperties(
|
||||
invoiceID = "123",
|
||||
seller = LocDataStructures.Company(
|
||||
name = "Mega Corp LTD.",
|
||||
address = "123 Main St. Awesome Town, ZZ 11111",
|
||||
phone = null
|
||||
),
|
||||
buyer = LocDataStructures.Company(
|
||||
name = "Sandworm Imports",
|
||||
address = "555 Elm St. Little Town, VV, 22222",
|
||||
phone = null
|
||||
),
|
||||
invoiceDate = INVOICE_TIME.atZone(ZoneOffset.UTC).toLocalDate(),
|
||||
term = 60,
|
||||
goods = arrayListOf<LocDataStructures.PricedGood>(
|
||||
LocDataStructures.PricedGood(
|
||||
description = "Salt",
|
||||
purchaseOrderRef = null,
|
||||
quantity = 10,
|
||||
unitPrice = 3.DOLLARS `issued by` defaultIssuer,
|
||||
grossWeight = null
|
||||
),
|
||||
LocDataStructures.PricedGood(
|
||||
description = "Pepper",
|
||||
purchaseOrderRef = null,
|
||||
quantity = 20,
|
||||
unitPrice = 4.DOLLARS `issued by` defaultIssuer,
|
||||
grossWeight = null
|
||||
)
|
||||
)
|
||||
)
|
||||
val initialInvoiceState = Invoice.State(MINI_CORP, ALICE, false,invoiceProperties)
|
||||
val initialAR = AccountReceivable.createARFromInvoice(initialInvoiceState, 0.9, notary)
|
||||
|
||||
enum class WhatKind {
|
||||
PAST, FUTURE
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
services = MockServices()
|
||||
}
|
||||
|
||||
fun generateInvoiceIssueTxn(kind: WhatKind = WhatKind.FUTURE): LedgerTransaction {
|
||||
val genTX: LedgerTransaction = run {
|
||||
val pastProp = initialInvoiceState.props.copy(invoiceDate =
|
||||
PAST_INVOICE_TIME.atZone(ZoneOffset.UTC).toLocalDate())
|
||||
|
||||
val invoice: Invoice.State = if (kind == WhatKind.PAST) {
|
||||
initialInvoiceState.copy(props = pastProp)
|
||||
} else {
|
||||
initialInvoiceState
|
||||
}
|
||||
|
||||
val gtx = invoice.generateInvoice(DUMMY_NOTARY).apply {
|
||||
setTime(TEST_TX_TIME, 30.seconds)
|
||||
signWith(MINI_CORP_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
gtx.toSignedTransaction().toLedgerTransaction(services)
|
||||
}
|
||||
genTX.verify()
|
||||
return genTX
|
||||
}
|
||||
|
||||
fun issuedInvoice(): Invoice.State {
|
||||
return generateInvoiceIssueTxn().outputs.filterIsInstance<Invoice.State>().single()
|
||||
}
|
||||
|
||||
fun issuedInvoiceWithPastDate(): Invoice.State {
|
||||
return generateInvoiceIssueTxn(WhatKind.PAST).outputs.filterIsInstance<Invoice.State>().single()
|
||||
}
|
||||
|
||||
// @Test
|
||||
fun `Apply - requireThat Tests`() {
|
||||
//Happy Path Apply
|
||||
transaction {
|
||||
input() { issuedInvoice() }
|
||||
output { issuedInvoice().copy(assigned = true) }
|
||||
output { initialAR.data }
|
||||
command(MINI_CORP_PUBKEY) { Invoice.Commands.Assign() }
|
||||
command(MEGA_CORP_PUBKEY) { AccountReceivable.Commands.Apply() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
verifies()
|
||||
}
|
||||
|
||||
transaction {
|
||||
output { initialAR.data }
|
||||
command(MEGA_CORP_PUBKEY) { AccountReceivable.Commands.Apply() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "Required net.corda.contracts.Invoice.Commands command"
|
||||
}
|
||||
|
||||
transaction {
|
||||
output { initialAR.data }
|
||||
command(MINI_CORP_PUBKEY) { Invoice.Commands.Assign() }
|
||||
command(MEGA_CORP_PUBKEY) { AccountReceivable.Commands.Apply() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "There must be an input Invoice state"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input() { issuedInvoice() }
|
||||
output { initialInvoiceState.copy(assigned = true) }
|
||||
output { initialAR.data }
|
||||
command(MINI_CORP_PUBKEY) { Invoice.Commands.Assign() }
|
||||
command(MEGA_CORP_PUBKEY) { AccountReceivable.Commands.Apply() }
|
||||
this `fails with` "must be timestamped"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input() { issuedInvoice() }
|
||||
output { initialInvoiceState.copy(assigned = true) }
|
||||
output { initialAR.data.copy(status = AccountReceivable.StatusEnum.Issued) }
|
||||
command(MEGA_CORP_PUBKEY) { AccountReceivable.Commands.Apply() }
|
||||
command(MINI_CORP_PUBKEY) { Invoice.Commands.Assign() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "AR state must be applied"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input() { issuedInvoice() }
|
||||
output { initialInvoiceState.copy(assigned = true) }
|
||||
output { initialAR.data.copy(props = initialAR.data.props.copy(invoiceID = "BOB")) }
|
||||
command(MEGA_CORP_PUBKEY) { AccountReceivable.Commands.Apply() }
|
||||
command(MINI_CORP_PUBKEY) { Invoice.Commands.Assign() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "AR properties must match input invoice"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input() { issuedInvoiceWithPastDate() }
|
||||
output { issuedInvoiceWithPastDate().copy(assigned = true) }
|
||||
output { AccountReceivable.createARFromInvoice(
|
||||
issuedInvoiceWithPastDate(), 0.9, notary).data }
|
||||
command(MEGA_CORP_PUBKEY) { AccountReceivable.Commands.Apply() }
|
||||
command(MINI_CORP_PUBKEY) { Invoice.Commands.Assign() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "the payment date must be in the future"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input() { issuedInvoice() }
|
||||
output { issuedInvoice().copy(assigned = true) }
|
||||
output { AccountReceivable.createARFromInvoice(
|
||||
issuedInvoice(), 1.9, notary).data }
|
||||
command(MEGA_CORP_PUBKEY) { AccountReceivable.Commands.Apply() }
|
||||
command(MINI_CORP_PUBKEY) { Invoice.Commands.Assign() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "The discount factor is invalid"
|
||||
}
|
||||
}
|
||||
|
||||
// @Test
|
||||
fun `Issue - requireThat Tests`() {
|
||||
//Happy Path Apply
|
||||
transaction {
|
||||
input() { AccountReceivable.createARFromInvoice(
|
||||
issuedInvoice(), 0.9, notary).data }
|
||||
output { AccountReceivable.createARFromInvoice(
|
||||
issuedInvoice(), 0.9, notary).data.copy(status = AccountReceivable.StatusEnum.Issued) }
|
||||
command(MEGA_CORP_PUBKEY) { AccountReceivable.Commands.Issue() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
verifies()
|
||||
}
|
||||
|
||||
transaction {
|
||||
input() { AccountReceivable.createARFromInvoice(
|
||||
issuedInvoice(), 0.9, notary).data.copy(status = AccountReceivable.StatusEnum.Issued) }
|
||||
output { AccountReceivable.createARFromInvoice(
|
||||
issuedInvoice(), 0.9, notary).data.copy(status = AccountReceivable.StatusEnum.Issued) }
|
||||
command(MEGA_CORP_PUBKEY) { AccountReceivable.Commands.Issue() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "input status must be applied"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input() { AccountReceivable.createARFromInvoice(
|
||||
issuedInvoice(), 0.9, notary).data.copy(status = AccountReceivable.StatusEnum.Applied) }
|
||||
output { AccountReceivable.createARFromInvoice(
|
||||
issuedInvoice(), 0.9, notary).data.copy(status = AccountReceivable.StatusEnum.Applied) }
|
||||
command(MEGA_CORP_PUBKEY) { AccountReceivable.Commands.Issue() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "output status must be issued"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input() { AccountReceivable.createARFromInvoice(
|
||||
issuedInvoice(), 0.9, notary).data.copy(status = AccountReceivable.StatusEnum.Applied) }
|
||||
output { AccountReceivable.createARFromInvoice(
|
||||
issuedInvoice(), 0.95, notary).data.copy(status = AccountReceivable.StatusEnum.Issued) }
|
||||
command(MEGA_CORP_PUBKEY) { AccountReceivable.Commands.Issue() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "properties must match"
|
||||
}
|
||||
}
|
||||
|
||||
// @Test
|
||||
fun `Extinguish - requireThat Tests`() {
|
||||
//Happy Path Extinguish
|
||||
transaction {
|
||||
input() { AccountReceivable.createARFromInvoice(
|
||||
issuedInvoiceWithPastDate(), 0.9, notary).data.copy(status = AccountReceivable.StatusEnum.Issued) }
|
||||
command(MEGA_CORP_PUBKEY) { AccountReceivable.Commands.Extinguish() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
verifies()
|
||||
}
|
||||
|
||||
transaction {
|
||||
input() { AccountReceivable.createARFromInvoice(
|
||||
issuedInvoice(), 0.9, notary).data.copy(status = AccountReceivable.StatusEnum.Issued) }
|
||||
command(MEGA_CORP_PUBKEY) { AccountReceivable.Commands.Extinguish() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "the payment date must be today or in the the past"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input() { AccountReceivable.createARFromInvoice(
|
||||
issuedInvoiceWithPastDate(), 0.9, notary).data.copy(status = AccountReceivable.StatusEnum.Applied) }
|
||||
command(MEGA_CORP_PUBKEY) { AccountReceivable.Commands.Extinguish() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "input status must be issued"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { AccountReceivable.createARFromInvoice(
|
||||
issuedInvoiceWithPastDate(), 0.9, notary).data.copy(status = AccountReceivable.StatusEnum.Issued) }
|
||||
output { AccountReceivable.createARFromInvoice(
|
||||
issuedInvoiceWithPastDate(), 0.9, notary).data.copy(status = AccountReceivable.StatusEnum.Issued) }
|
||||
command(MEGA_CORP_PUBKEY) { AccountReceivable.Commands.Extinguish() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "output state must not exist"
|
||||
}
|
||||
}
|
||||
|
||||
// @Test
|
||||
fun ok() {
|
||||
// createARAndSendToBank().verify()
|
||||
}
|
||||
|
||||
val START_TIME = Instant.parse("2015-04-17T12:00:00.00Z")
|
||||
val APPLY_TIME = Instant.parse("2015-04-17T12:05:00.00Z")
|
||||
val ISSUE_TIME = Instant.parse("2015-04-17T12:15:00.00Z")
|
||||
val END_TIME = Instant.parse("2015-04-27T12:00:00.00Z")
|
||||
|
||||
/*
|
||||
private fun createARAndSendToBank(): TransactionGroupDSL<AccountReceivable.State> {
|
||||
|
||||
return transactionGroupFor {
|
||||
roots {
|
||||
transaction(99.DOLLARS.CASH `issued by` defaultIssuer`owned by` MEGA_CORP_PUBKEY `with notary` DUMMY_NOTARY label "bank's money")
|
||||
transaction(110.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY label "buyer's money")
|
||||
}
|
||||
|
||||
val newProps = invoiceProperties.copy(invoiceDate = START_TIME.atZone(ZoneOffset.UTC).toLocalDate(),
|
||||
term = 5)
|
||||
val newInvoice = initialInvoiceState.copy(props = newProps)
|
||||
val ar = AccountReceivable.createARFromInvoice(newInvoice, 0.90, notary)
|
||||
|
||||
// 1. Create new invoice
|
||||
transaction {
|
||||
output("new invoice") { newInvoice }
|
||||
command(MINI_CORP_PUBKEY) { Invoice.Commands.Issue() }
|
||||
timestamp(START_TIME)
|
||||
}
|
||||
|
||||
// 2. create new AR
|
||||
transaction {
|
||||
input("new invoice")
|
||||
output("applied invoice") { initialInvoiceState.copy(assigned=true, props = newProps) }
|
||||
output("new AR") { ar.data }
|
||||
command(MINI_CORP_PUBKEY) { Invoice.Commands.Assign() }
|
||||
command(MEGA_CORP_PUBKEY) { AccountReceivable.Commands.Apply() }
|
||||
timestamp(APPLY_TIME)
|
||||
}
|
||||
|
||||
// 3. issue AR
|
||||
transaction {
|
||||
input ("new AR")
|
||||
input("bank's money")
|
||||
output ("issued AR") {
|
||||
ar.data.copy(status=AccountReceivable.StatusEnum.Issued)
|
||||
}
|
||||
output { 99.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY }
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
command(MINI_CORP_PUBKEY) { AccountReceivable.Commands.Issue() }
|
||||
timestamp(ISSUE_TIME)
|
||||
}
|
||||
|
||||
// 4. extinguish AR
|
||||
transaction {
|
||||
input ("applied invoice")
|
||||
input ("issued AR")
|
||||
input ("buyer's money")
|
||||
output { 110.DOLLARS.CASH `owned by` MEGA_CORP_PUBKEY }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move() }
|
||||
command(MINI_CORP_PUBKEY) { Invoice.Commands.Extinguish() }
|
||||
command(MEGA_CORP_PUBKEY) { AccountReceivable.Commands.Extinguish() }
|
||||
timestamp(END_TIME)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
@ -1,354 +0,0 @@
|
||||
package net.corda.contracts
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
|
||||
|
||||
class BillOfLadingAgreementTests {
|
||||
val pros = BillOfLadingAgreement.BillOfLadingProperties(
|
||||
billOfLadingID = "billOfLadingID",
|
||||
issueDate = LocalDate.now(),
|
||||
carrierOwner = ALICE,
|
||||
nameOfVessel = "Karaboudjan",
|
||||
descriptionOfGoods = listOf(LocDataStructures.Good(description="Crab meet cans",quantity = 10000,grossWeight = null)),
|
||||
dateOfShipment = LocalDate.now(),
|
||||
portOfLoading = LocDataStructures.Port(country = "Morokko",city = "Larache",address = null,state = null,name=null),
|
||||
portOfDischarge = LocDataStructures.Port(country = "Belgium",city = "Antwerpen",address = null,state = null,name=null),
|
||||
shipper = null,
|
||||
notify = LocDataStructures.Person(
|
||||
name = "Some guy",
|
||||
address = "Some address",
|
||||
phone = "+11 23456789"
|
||||
),
|
||||
consignee = LocDataStructures.Company(
|
||||
name = "Some company",
|
||||
address = "Some other address",
|
||||
phone = "+11 12345678"
|
||||
),
|
||||
grossWeight = LocDataStructures.Weight(
|
||||
quantity = 2500.0,
|
||||
unit = LocDataStructures.WeightUnit.KG
|
||||
)
|
||||
)
|
||||
val Bill = BillOfLadingAgreement.State(
|
||||
owner = MEGA_CORP_PUBKEY,
|
||||
beneficiary = BOB,
|
||||
props =pros
|
||||
)
|
||||
|
||||
lateinit var services: MockServices
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
services = MockServices()
|
||||
}
|
||||
|
||||
//Generation method tests
|
||||
|
||||
@Test
|
||||
fun issueGenerationMethod() {
|
||||
val ptx = BillOfLadingAgreement().generateIssue(Bill.owner, Bill.beneficiary, Bill.props, DUMMY_NOTARY).apply {
|
||||
signWith(ALICE_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun issueGenerationMethod_Unsigned() {
|
||||
val ptx = BillOfLadingAgreement().generateIssue(Bill.owner, Bill.beneficiary, Bill.props, DUMMY_NOTARY)
|
||||
val stx = ptx.toSignedTransaction()
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun issueGenerationMethod_KeyMismatch() {
|
||||
val ptx = BillOfLadingAgreement().generateIssue(Bill.owner, Bill.beneficiary, Bill.props, DUMMY_NOTARY).apply {
|
||||
signWith(BOB_KEY)
|
||||
}
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
// @Test // TODO: Fix Test
|
||||
fun transferAndEndorseGenerationMethod() {
|
||||
|
||||
val ptx: TransactionBuilder = TransactionType.General.Builder(notary = DUMMY_NOTARY)
|
||||
val sr = StateAndRef(
|
||||
TransactionState(Bill, DUMMY_NOTARY),
|
||||
StateRef(SecureHash.randomSHA256(), Random().nextInt(32))
|
||||
)
|
||||
BillOfLadingAgreement().generateTransferAndEndorse(ptx,sr,CHARLIE_PUBKEY, CHARLIE)
|
||||
ptx.signWith(MEGA_CORP_KEY) //Signed by owner
|
||||
ptx.signWith(BOB_KEY) //and beneficiary
|
||||
// ptx.signWith(CHARLIE_KEY) // ??????
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun transferAndEndorseGenerationMethod_MissingBeneficiarySignature() {
|
||||
val ptx: TransactionBuilder = TransactionType.General.Builder(notary = DUMMY_NOTARY)
|
||||
val sr = StateAndRef(
|
||||
TransactionState(Bill, DUMMY_NOTARY),
|
||||
StateRef(SecureHash.randomSHA256(), Random().nextInt(32))
|
||||
)
|
||||
BillOfLadingAgreement().generateTransferAndEndorse(ptx,sr,CHARLIE_PUBKEY, CHARLIE)
|
||||
ptx.signWith(MEGA_CORP_KEY) //Signed by owner
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun transferAndEndorseGenerationMethod_MissingOwnerSignature() {
|
||||
val ptx: TransactionBuilder = TransactionType.General.Builder(notary = DUMMY_NOTARY)
|
||||
val sr = StateAndRef(
|
||||
TransactionState(Bill, DUMMY_NOTARY),
|
||||
StateRef(SecureHash.randomSHA256(), Random().nextInt(32))
|
||||
)
|
||||
BillOfLadingAgreement().generateTransferAndEndorse(ptx,sr,CHARLIE_PUBKEY, CHARLIE)
|
||||
ptx.signWith(BOB_KEY) //Signed by beneficiary
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
// @Test // TODO Fix Test
|
||||
fun transferPossessionGenerationMethod() {
|
||||
val ptx: TransactionBuilder = TransactionType.General.Builder(notary = DUMMY_NOTARY)
|
||||
val sr = StateAndRef(
|
||||
TransactionState(Bill, DUMMY_NOTARY),
|
||||
StateRef(SecureHash.randomSHA256(), Random().nextInt(32))
|
||||
)
|
||||
BillOfLadingAgreement().generateTransferPossession(ptx,sr,CHARLIE_PUBKEY)
|
||||
ptx.signWith(MEGA_CORP_KEY) //Signed by owner
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun transferPossessionGenerationMethod_Unsigned() {
|
||||
val ptx: TransactionBuilder = TransactionType.General.Builder(notary = DUMMY_NOTARY)
|
||||
val sr = StateAndRef(
|
||||
TransactionState(Bill, DUMMY_NOTARY),
|
||||
StateRef(SecureHash.randomSHA256(), Random().nextInt(32))
|
||||
)
|
||||
BillOfLadingAgreement().generateTransferPossession(ptx,sr,CHARLIE_PUBKEY)
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
//Custom transaction tests
|
||||
|
||||
@Test
|
||||
fun generalConsistencyTests() {
|
||||
transaction {
|
||||
input { Bill }
|
||||
output { Bill.copy(owner = CHARLIE_PUBKEY, beneficiary = CHARLIE) }
|
||||
command(MEGA_CORP_PUBKEY, BOB_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
command(MEGA_CORP_PUBKEY) { BillOfLadingAgreement.Commands.TransferPossession() }
|
||||
timestamp(Instant.now())
|
||||
//There are multiple commands
|
||||
this `fails with` "List has more than one element."
|
||||
}
|
||||
transaction {
|
||||
input { Bill }
|
||||
output { Bill.copy(owner = CHARLIE_PUBKEY, beneficiary = CHARLIE) }
|
||||
timestamp(Instant.now())
|
||||
//There are no commands
|
||||
this `fails with` "Required ${BillOfLadingAgreement.Commands::class.qualifiedName} command"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun issueTests() {
|
||||
transaction {
|
||||
output { Bill }
|
||||
command(ALICE_PUBKEY) { BillOfLadingAgreement.Commands.IssueBL() }
|
||||
timestamp(Instant.now())
|
||||
this.verifies()
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { Bill }
|
||||
output { Bill.copy(owner = CHARLIE_PUBKEY, beneficiary = CHARLIE) }
|
||||
command(MEGA_CORP_PUBKEY, BOB_PUBKEY) { BillOfLadingAgreement.Commands.IssueBL() }
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "there is no input state"
|
||||
}
|
||||
|
||||
transaction {
|
||||
output { Bill }
|
||||
command(BOB_PUBKEY) { BillOfLadingAgreement.Commands.IssueBL() }
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the transaction is signed by the carrier"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun transferAndEndorseTests() {
|
||||
transaction {
|
||||
input { Bill }
|
||||
output { Bill.copy(owner = CHARLIE_PUBKEY, beneficiary = CHARLIE) }
|
||||
command(MEGA_CORP_PUBKEY, BOB_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
timestamp(Instant.now())
|
||||
this.verifies()
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { Bill }
|
||||
output { Bill.copy(owner = CHARLIE_PUBKEY, beneficiary = CHARLIE) }
|
||||
command(MEGA_CORP_PUBKEY, BOB_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
//There is no timestamp
|
||||
this `fails with` "must be timestamped"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { Bill }
|
||||
input { Bill.copy(owner = CHARLIE_PUBKEY, beneficiary = CHARLIE) }
|
||||
output { Bill.copy(owner = CHARLIE_PUBKEY, beneficiary = CHARLIE) }
|
||||
command(MEGA_CORP_PUBKEY, BOB_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
timestamp(Instant.now())
|
||||
//There are two inputs
|
||||
this `fails with` "List has more than one element."
|
||||
}
|
||||
|
||||
transaction {
|
||||
output { Bill.copy(owner = CHARLIE_PUBKEY, beneficiary = CHARLIE) }
|
||||
command(MEGA_CORP_PUBKEY, BOB_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
timestamp(Instant.now())
|
||||
//There are no inputs
|
||||
this `fails with` "List is empty."
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { Bill }
|
||||
output { Bill.copy(owner = CHARLIE_PUBKEY, beneficiary = CHARLIE) }
|
||||
output { Bill }
|
||||
command(MEGA_CORP_PUBKEY, BOB_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
timestamp(Instant.now())
|
||||
//There are two outputs
|
||||
this `fails with` "List has more than one element."
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { Bill }
|
||||
command(MEGA_CORP_PUBKEY, BOB_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
timestamp(Instant.now())
|
||||
//There are no outputs
|
||||
this `fails with` "List is empty."
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { Bill }
|
||||
output { Bill.copy(owner = CHARLIE_PUBKEY, beneficiary = CHARLIE) }
|
||||
command(MEGA_CORP_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the transaction is signed by the beneficiary"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { Bill }
|
||||
output { Bill.copy(owner = CHARLIE_PUBKEY, beneficiary = CHARLIE) }
|
||||
command(BOB_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the transaction is signed by the state object owner"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { Bill }
|
||||
output { Bill.copy(owner = CHARLIE_PUBKEY, beneficiary = CHARLIE, props = pros.copy(nameOfVessel = "Svet")) }
|
||||
command(MEGA_CORP_PUBKEY, BOB_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the bill of lading agreement properties are unchanged"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun transferPossessionTests() {
|
||||
transaction {
|
||||
input { Bill }
|
||||
output { Bill.copy(owner = CHARLIE_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY) { BillOfLadingAgreement.Commands.TransferPossession() }
|
||||
timestamp(Instant.now())
|
||||
this.verifies()
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { Bill }
|
||||
output { Bill.copy(owner = CHARLIE_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY) { BillOfLadingAgreement.Commands.TransferPossession() }
|
||||
//There is no timestamp
|
||||
this `fails with` "must be timestamped"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { Bill }
|
||||
input { Bill.copy(owner = BOB_PUBKEY) }
|
||||
output { Bill.copy(owner = CHARLIE_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY) { BillOfLadingAgreement.Commands.TransferPossession() }
|
||||
timestamp(Instant.now())
|
||||
//There are two inputs
|
||||
this `fails with` "List has more than one element."
|
||||
}
|
||||
|
||||
transaction {
|
||||
output { Bill.copy(owner = CHARLIE_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY) { BillOfLadingAgreement.Commands.TransferPossession() }
|
||||
timestamp(Instant.now())
|
||||
//There are no inputs
|
||||
this `fails with` "List is empty."
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { Bill }
|
||||
output { Bill.copy(owner = CHARLIE_PUBKEY) }
|
||||
output { Bill.copy(owner = ALICE_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY) { BillOfLadingAgreement.Commands.TransferPossession() }
|
||||
timestamp(Instant.now())
|
||||
//There are two outputs
|
||||
this `fails with` "List has more than one element."
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { Bill }
|
||||
command(MEGA_CORP_PUBKEY) { BillOfLadingAgreement.Commands.TransferPossession() }
|
||||
timestamp(Instant.now())
|
||||
//There are no outputs
|
||||
this `fails with` "List is empty."
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { Bill }
|
||||
output { Bill.copy(owner = CHARLIE_PUBKEY) }
|
||||
command(ALICE_PUBKEY) { BillOfLadingAgreement.Commands.TransferPossession() }
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the transaction is signed by the state object owner"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { Bill }
|
||||
output { Bill.copy(owner = CHARLIE_PUBKEY,beneficiary = CHARLIE) }
|
||||
command(MEGA_CORP_PUBKEY) { BillOfLadingAgreement.Commands.TransferPossession() }
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the beneficiary is unchanged"
|
||||
}
|
||||
|
||||
|
||||
transaction {
|
||||
input { Bill }
|
||||
output { Bill.copy(owner = CHARLIE_PUBKEY, props = pros.copy(nameOfVessel = "Svet")) }
|
||||
command(MEGA_CORP_PUBKEY) { BillOfLadingAgreement.Commands.TransferPossession() }
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the bill of lading agreement properties are unchanged"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,253 +0,0 @@
|
||||
package net.corda.contracts
|
||||
|
||||
import net.corda.core.contracts.DOLLARS
|
||||
import net.corda.core.contracts.`issued by`
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.utilities.DUMMY_PUBKEY_1
|
||||
import net.corda.core.utilities.TEST_TX_TIME
|
||||
import net.corda.testing.*
|
||||
import org.junit.Test
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
import kotlin.test.fail
|
||||
|
||||
class InvoiceTests {
|
||||
|
||||
val defaultRef = OpaqueBytes(ByteArray(1, { 1 }))
|
||||
val defaultIssuer = MEGA_CORP.ref(defaultRef)
|
||||
|
||||
val invoiceProperties = Invoice.InvoiceProperties(
|
||||
invoiceID = "123",
|
||||
seller = LocDataStructures.Company(
|
||||
name = "Mega Corp LTD.",
|
||||
address = "123 Main St. Awesome Town, ZZ 11111",
|
||||
phone = null
|
||||
),
|
||||
buyer = LocDataStructures.Company(
|
||||
name = "Sandworm Imports",
|
||||
address = "555 Elm St. Little Town, VV, 22222",
|
||||
phone = null
|
||||
),
|
||||
invoiceDate = LocalDate.now(),
|
||||
term = 60,
|
||||
goods = arrayListOf<LocDataStructures.PricedGood>(
|
||||
LocDataStructures.PricedGood(
|
||||
description = "Salt",
|
||||
purchaseOrderRef = null,
|
||||
quantity = 10,
|
||||
unitPrice = 3.DOLLARS `issued by` defaultIssuer,
|
||||
grossWeight = null
|
||||
),
|
||||
LocDataStructures.PricedGood(
|
||||
description = "Pepper",
|
||||
purchaseOrderRef = null,
|
||||
quantity = 20,
|
||||
unitPrice = 4.DOLLARS `issued by` defaultIssuer,
|
||||
grossWeight = null
|
||||
)
|
||||
)
|
||||
)
|
||||
val initialInvoiceState = Invoice.State(MEGA_CORP, ALICE, false,invoiceProperties)
|
||||
|
||||
@Test
|
||||
fun `Issue - requireThat Tests`() {
|
||||
|
||||
//Happy Path Issue
|
||||
transaction {
|
||||
output { initialInvoiceState }
|
||||
command(MEGA_CORP_PUBKEY) { Invoice.Commands.Issue() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
verifies()
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { initialInvoiceState }
|
||||
output { initialInvoiceState }
|
||||
command(MEGA_CORP_PUBKEY) { Invoice.Commands.Issue() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "there is no input state"
|
||||
}
|
||||
|
||||
transaction {
|
||||
output { initialInvoiceState }
|
||||
command(DUMMY_PUBKEY_1) { Invoice.Commands.Issue() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "the transaction is signed by the invoice owner"
|
||||
}
|
||||
|
||||
var props = invoiceProperties.copy(seller = invoiceProperties.buyer)
|
||||
transaction {
|
||||
output { initialInvoiceState.copy(props = props) }
|
||||
command(MEGA_CORP_PUBKEY) { Invoice.Commands.Issue() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "the buyer and seller must be different"
|
||||
}
|
||||
|
||||
transaction {
|
||||
output { initialInvoiceState.copy(assigned = true) }
|
||||
command(MEGA_CORP_PUBKEY) { Invoice.Commands.Issue() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "the invoice must not be assigned"
|
||||
}
|
||||
|
||||
props = invoiceProperties.copy(invoiceID = "")
|
||||
transaction {
|
||||
output { initialInvoiceState.copy(props = props) }
|
||||
command(MEGA_CORP_PUBKEY) { Invoice.Commands.Issue() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "the invoice ID must not be blank"
|
||||
}
|
||||
|
||||
val withMessage = "the term must be a positive number"
|
||||
val r = try {
|
||||
props = invoiceProperties.copy(term = 0)
|
||||
false
|
||||
} catch (e: Exception) {
|
||||
val m = e.message
|
||||
if (m == null)
|
||||
fail("Threw exception without a message")
|
||||
else
|
||||
if (!m.toLowerCase().contains(withMessage.toLowerCase())) throw AssertionError("Error was actually: $m", e)
|
||||
true
|
||||
}
|
||||
if (!r) throw AssertionError("Expected exception but didn't get one")
|
||||
|
||||
props = invoiceProperties.copy(invoiceDate = LocalDate.now().minusDays(invoiceProperties.term + 1))
|
||||
transaction {
|
||||
output { initialInvoiceState.copy(props = props) }
|
||||
command(MEGA_CORP_PUBKEY) { Invoice.Commands.Issue() }
|
||||
timestamp(java.time.Instant.now())
|
||||
this `fails with` "the payment date must be in the future"
|
||||
}
|
||||
|
||||
val withMessage2 = "there must be goods assigned to the invoice"
|
||||
val r2 = try {
|
||||
props = invoiceProperties.copy(goods = Collections.emptyList())
|
||||
false
|
||||
} catch (e: Exception) {
|
||||
val m = e.message
|
||||
if (m == null)
|
||||
fail("Threw exception without a message")
|
||||
else
|
||||
if (!m.toLowerCase().contains(withMessage2.toLowerCase())) {
|
||||
throw AssertionError("Error was actually: $m expected $withMessage2", e)
|
||||
}
|
||||
true
|
||||
}
|
||||
if (!r2) throw AssertionError("Expected exception but didn't get one")
|
||||
|
||||
val goods = arrayListOf<LocDataStructures.PricedGood>( LocDataStructures.PricedGood(
|
||||
description = "Salt",
|
||||
purchaseOrderRef = null,
|
||||
quantity = 10,
|
||||
unitPrice = 0.DOLLARS `issued by` defaultIssuer,
|
||||
grossWeight = null
|
||||
))
|
||||
|
||||
props = invoiceProperties.copy(goods = goods)
|
||||
transaction {
|
||||
output { initialInvoiceState.copy(props = props) }
|
||||
command(MEGA_CORP_PUBKEY) { Invoice.Commands.Issue() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "the invoice amount must be non-zero"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Assign - requireThat Tests`() {
|
||||
|
||||
//Happy Path Assign
|
||||
transaction {
|
||||
input { initialInvoiceState }
|
||||
output { initialInvoiceState.copy(assigned = true) }
|
||||
command(MEGA_CORP_PUBKEY) { Invoice.Commands.Assign() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
verifies()
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { initialInvoiceState }
|
||||
output { initialInvoiceState.copy(owner = ALICE) }
|
||||
command(MEGA_CORP_PUBKEY) { Invoice.Commands.Assign() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "input state owner must be the same as the output state owner"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { initialInvoiceState }
|
||||
output { initialInvoiceState }
|
||||
command(DUMMY_PUBKEY_1) { Invoice.Commands.Assign() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "the transaction must be signed by the owner"
|
||||
}
|
||||
|
||||
var props = invoiceProperties.copy(seller = invoiceProperties.buyer)
|
||||
transaction {
|
||||
input { initialInvoiceState }
|
||||
output { initialInvoiceState.copy(props = props) }
|
||||
command(MEGA_CORP_PUBKEY) { Invoice.Commands.Assign() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "the invoice properties must remain unchanged"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { initialInvoiceState.copy(assigned = true) }
|
||||
output { initialInvoiceState }
|
||||
command(MEGA_CORP_PUBKEY) { Invoice.Commands.Assign() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "the input invoice must not be assigned"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { initialInvoiceState }
|
||||
output { initialInvoiceState.copy(assigned = false) }
|
||||
command(MEGA_CORP_PUBKEY) { Invoice.Commands.Assign() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "the output invoice must be assigned"
|
||||
}
|
||||
|
||||
props = invoiceProperties.copy(invoiceDate = LocalDate.now().minusDays(invoiceProperties.term + 1))
|
||||
transaction {
|
||||
input { initialInvoiceState.copy(props = props) }
|
||||
output { initialInvoiceState.copy(props = props, assigned = true) }
|
||||
command(MEGA_CORP_PUBKEY) { Invoice.Commands.Assign() }
|
||||
timestamp(java.time.Instant.now())
|
||||
this `fails with` "the payment date must be in the future"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Extinguish - requireThat Tests`() {
|
||||
|
||||
//Happy Path Extinguish
|
||||
val props = invoiceProperties.copy(invoiceDate = LocalDate.now().minusDays(invoiceProperties.term + 1))
|
||||
transaction {
|
||||
input { initialInvoiceState.copy(props = props) }
|
||||
command(MEGA_CORP_PUBKEY) { Invoice.Commands.Extinguish() }
|
||||
timestamp(java.time.Instant.now())
|
||||
verifies()
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { initialInvoiceState }
|
||||
output { initialInvoiceState }
|
||||
command(MEGA_CORP_PUBKEY) { Invoice.Commands.Extinguish() }
|
||||
timestamp(java.time.Instant.now())
|
||||
this `fails with` "there shouldn't be an output state"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { initialInvoiceState }
|
||||
command(DUMMY_PUBKEY_1) { Invoice.Commands.Extinguish() }
|
||||
timestamp(java.time.Instant.now())
|
||||
this `fails with` "the transaction must be signed by the owner"
|
||||
}
|
||||
|
||||
// transaction {
|
||||
// input { initialInvoiceState }
|
||||
// command(MEGA_CORP_PUBKEY) { Invoice.Commands.Extinguish() }
|
||||
// timestamp(java.time.Instant.now())
|
||||
// this `fails requirement` "the payment date must be today or in the past"
|
||||
// }
|
||||
}
|
||||
}
|
@ -1,470 +0,0 @@
|
||||
package net.corda.contracts
|
||||
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.Period
|
||||
|
||||
|
||||
class LOCTests {
|
||||
|
||||
val defaultRef = OpaqueBytes(ByteArray(1, { 1 }))
|
||||
val defaultIssuer = MEGA_CORP.ref(defaultRef)
|
||||
|
||||
val pros = LOC.LOCProperties(
|
||||
letterOfCreditID = "letterOfCreditID",
|
||||
applicationDate = LocalDate.of(2016,5,15),
|
||||
issueDate = LocalDate.now().minusDays(30),
|
||||
typeCredit = LocDataStructures.CreditType.SIGHT,
|
||||
amount = 100000.DOLLARS `issued by` defaultIssuer,
|
||||
expiryDate = LocalDate.now().plusDays(1),
|
||||
portLoading = LocDataStructures.Port("SG","Singapore",null,null,null),
|
||||
portDischarge = LocDataStructures.Port("US","Oakland",null,null,null),
|
||||
descriptionGoods = listOf(LocDataStructures.PricedGood(description="Tiger balm",
|
||||
quantity = 10000,
|
||||
grossWeight = null,
|
||||
unitPrice = 1.DOLLARS `issued by` defaultIssuer,
|
||||
purchaseOrderRef = null
|
||||
)),
|
||||
placePresentation = LocDataStructures.Location("US","California","Oakland"),
|
||||
latestShip = LocalDate.of(2016,6,12),
|
||||
periodPresentation = Period.ofDays(31),
|
||||
beneficiary = ALICE,
|
||||
issuingbank = MEGA_CORP,
|
||||
appplicant = CHARLIE,
|
||||
invoiceRef = StateRef(SecureHash.randomSHA256(),0)
|
||||
)
|
||||
|
||||
val LOCstate = LOC.State(
|
||||
beneficiaryPaid = false,
|
||||
issued = false,
|
||||
terminated = false,
|
||||
props =pros
|
||||
)
|
||||
|
||||
|
||||
val Billpros = BillOfLadingAgreement.BillOfLadingProperties(
|
||||
billOfLadingID = "billOfLadingID",
|
||||
issueDate = LocalDate.of(2016,6,1),
|
||||
carrierOwner = BOB,
|
||||
nameOfVessel = "Karaboudjan",
|
||||
descriptionOfGoods = listOf(LocDataStructures.Good(description="Crab meet cans",quantity = 10000,grossWeight = null)),
|
||||
dateOfShipment = null,
|
||||
portOfLoading = LocDataStructures.Port(country = "Morokko",city = "Larache",address = null,name = null,state = null),
|
||||
portOfDischarge = LocDataStructures.Port(country = "Belgium",city = "Antwerpen",address = null, name = null, state = null),
|
||||
shipper = null,
|
||||
notify = LocDataStructures.Person(
|
||||
name = "Some guy",
|
||||
address = "Some address",
|
||||
phone = "+11 23456789"
|
||||
),
|
||||
consignee = LocDataStructures.Company(
|
||||
name = "Some company",
|
||||
address = "Some other address",
|
||||
phone = "+11 12345678"
|
||||
),
|
||||
grossWeight = LocDataStructures.Weight(
|
||||
quantity = 2500.0,
|
||||
unit = LocDataStructures.WeightUnit.KG
|
||||
)
|
||||
)
|
||||
|
||||
val Billstate = BillOfLadingAgreement.State(
|
||||
owner = ALICE_PUBKEY,
|
||||
beneficiary = ALICE,
|
||||
props =Billpros
|
||||
)
|
||||
|
||||
val Cashstate = Cash.State(
|
||||
deposit = MEGA_CORP.ref(1),
|
||||
amount = 100000.DOLLARS,
|
||||
owner = MEGA_CORP_PUBKEY
|
||||
)
|
||||
|
||||
val invoiceState = Invoice.State(
|
||||
owner = ALICE,
|
||||
buyer = BOB,
|
||||
assigned = true,
|
||||
props = Invoice.InvoiceProperties(
|
||||
invoiceID = "test",
|
||||
seller = LocDataStructures.Company(
|
||||
name = "Alice",
|
||||
address = "",
|
||||
phone = null
|
||||
),
|
||||
buyer = LocDataStructures.Company(
|
||||
name = "Charlie",
|
||||
address = "",
|
||||
phone = null
|
||||
),
|
||||
invoiceDate = LocalDate.now().minusDays(1),
|
||||
term = 1,
|
||||
goods = listOf(LocDataStructures.PricedGood(
|
||||
description = "Test good",
|
||||
purchaseOrderRef = null,
|
||||
quantity = 1000,
|
||||
unitPrice = 100.DOLLARS `issued by` defaultIssuer,
|
||||
grossWeight = null)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
lateinit var services: MockServices
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
services = MockServices()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun issueSignedByBank() {
|
||||
val ptx = LOC().generateIssue(LOCstate.beneficiaryPaid, true, LOCstate.terminated, LOCstate.props, DUMMY_NOTARY).apply {
|
||||
signWith(MEGA_CORP_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun issueUnsigned() {
|
||||
val ptx = LOC().generateIssue(LOCstate.beneficiaryPaid, LOCstate.issued, LOCstate.terminated, LOCstate.props, DUMMY_NOTARY)
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun issueKeyMismatch() {
|
||||
val ptx = LOC().generateIssue(LOCstate.beneficiaryPaid, LOCstate.issued, LOCstate.terminated, LOCstate.props, DUMMY_NOTARY).apply {
|
||||
signWith(BOB_KEY)
|
||||
}
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun issueStatusTests() {
|
||||
|
||||
transaction {
|
||||
output { LOCstate.copy(issued = false) }
|
||||
command(MEGA_CORP_PUBKEY) { LOC.Commands.Issuance() }
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the LOC must be Issued"
|
||||
}
|
||||
transaction {
|
||||
output { LOCstate.copy(beneficiaryPaid = true, issued = true) }
|
||||
command(MEGA_CORP_PUBKEY) { LOC.Commands.Issuance() }
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "Demand Presentation must not be preformed successfully"
|
||||
}
|
||||
transaction {
|
||||
output { LOCstate.copy(terminated = true, issued = true) }
|
||||
command(MEGA_CORP_PUBKEY) { LOC.Commands.Issuance() }
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "LOC must not be terminated"
|
||||
}
|
||||
transaction {
|
||||
output { LOCstate.copy(issued = true) }
|
||||
command(MEGA_CORP_PUBKEY) { LOC.Commands.Issuance() }
|
||||
timestamp(Instant.now())
|
||||
this.verifies()
|
||||
}
|
||||
transaction {
|
||||
output { LOCstate.copy(issued = true, props = pros.copy(periodPresentation = Period.ofDays(0))) }
|
||||
// output { LOCstate.copy() }
|
||||
command(MEGA_CORP_PUBKEY) { LOC.Commands.Issuance() }
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the period of presentation must be a positive number"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun demandPresentaionTests() {
|
||||
transaction {
|
||||
input { LOCstate.copy(issued = true) }
|
||||
input { Billstate }
|
||||
input { Cashstate }
|
||||
input { invoiceState }
|
||||
output { LOCstate.copy(beneficiaryPaid = true, issued = true)}
|
||||
output { Billstate.copy(beneficiary = CHARLIE)}
|
||||
output { Cashstate.copy(owner = ALICE_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY, ALICE_PUBKEY) { LOC.Commands.DemandPresentation() }
|
||||
command(ALICE_PUBKEY) { Invoice.Commands.Extinguish()}
|
||||
command(ALICE_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
command(MEGA_CORP_PUBKEY) {Cash.Commands.Move()}
|
||||
timestamp(Instant.now())
|
||||
this.verifies()
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { LOCstate.copy(issued = true) }
|
||||
input { Billstate }
|
||||
input { Cashstate }
|
||||
input { invoiceState }
|
||||
output { LOCstate.copy(beneficiaryPaid = true, issued = true)}
|
||||
output { Billstate.copy(beneficiary = CHARLIE)}
|
||||
output { Cashstate.copy(owner = ALICE_PUBKEY) }
|
||||
command(ALICE_PUBKEY) { LOC.Commands.DemandPresentation() }
|
||||
command(ALICE_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
command(ALICE_PUBKEY) { Invoice.Commands.Extinguish()}
|
||||
command(MEGA_CORP_PUBKEY) {Cash.Commands.Move()}
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the transaction is signed by the issuing bank"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { LOCstate.copy(issued = true) }
|
||||
input { Billstate }
|
||||
input { Cashstate }
|
||||
input { invoiceState }
|
||||
output { LOCstate.copy(beneficiaryPaid = true, issued = true)}
|
||||
output { Billstate.copy(beneficiary = CHARLIE)}
|
||||
output { Cashstate.copy(owner = ALICE_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY) { LOC.Commands.DemandPresentation() }
|
||||
command(ALICE_PUBKEY) { Invoice.Commands.Extinguish()}
|
||||
command(ALICE_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
command(MEGA_CORP_PUBKEY) {Cash.Commands.Move()}
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the transaction is signed by the Beneficiary"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { LOCstate.copy(issued = true) }
|
||||
input { Billstate }
|
||||
input { Cashstate }
|
||||
input { invoiceState }
|
||||
output { LOCstate.copy(beneficiaryPaid = true, issued = true, props = pros.copy(amount = 1.POUNDS `issued by` defaultIssuer ))}
|
||||
output { Billstate.copy(owner = CHARLIE_PUBKEY)}
|
||||
output { Cashstate.copy(owner = ALICE_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY, ALICE_PUBKEY) { LOC.Commands.DemandPresentation() }
|
||||
command(ALICE_PUBKEY) { Invoice.Commands.Extinguish()}
|
||||
command(ALICE_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
command(MEGA_CORP_PUBKEY) {Cash.Commands.Move()}
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the LOC properties do not remain the same"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { LOCstate.copy(issued = true, props = pros.copy(latestShip = Billstate.props.issueDate.minusDays(1))) }
|
||||
input { Billstate }
|
||||
input { Cashstate }
|
||||
input { invoiceState }
|
||||
output { LOCstate.copy(beneficiaryPaid = true, issued = true, props = pros.copy(latestShip = Billstate.props.issueDate.minusDays(1)))}
|
||||
output { Billstate.copy(beneficiary = CHARLIE)}
|
||||
output { Cashstate.copy(owner = ALICE_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY, ALICE_PUBKEY) { LOC.Commands.DemandPresentation() }
|
||||
command(ALICE_PUBKEY) { Invoice.Commands.Extinguish()}
|
||||
command(ALICE_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
command(MEGA_CORP_PUBKEY) {Cash.Commands.Move()}
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the shipment is late"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { LOCstate.copy(issued = true) }
|
||||
input { Billstate }
|
||||
input { Cashstate.copy(amount = 99000.DOLLARS `issued by` defaultIssuer) }
|
||||
input { invoiceState }
|
||||
output { LOCstate.copy(beneficiaryPaid = true, issued = true)}
|
||||
output { Billstate.copy(owner = CHARLIE_PUBKEY)}
|
||||
output { Cashstate.copy(amount = 99000.DOLLARS `issued by` defaultIssuer ).copy(owner = ALICE_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY, ALICE_PUBKEY) { LOC.Commands.DemandPresentation() }
|
||||
command(ALICE_PUBKEY) { Invoice.Commands.Extinguish()}
|
||||
command(ALICE_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
command(MEGA_CORP_PUBKEY) {Cash.Commands.Move()}
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the cash state has not been transferred"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { LOCstate.copy(issued = true) }
|
||||
input { Billstate }
|
||||
input { Cashstate }
|
||||
input { invoiceState }
|
||||
output { LOCstate.copy(beneficiaryPaid = true, issued = true)}
|
||||
output { Billstate.copy(beneficiary = ALICE)}
|
||||
output { Cashstate.copy(owner = ALICE_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY, ALICE_PUBKEY) { LOC.Commands.DemandPresentation() }
|
||||
command(ALICE_PUBKEY) { Invoice.Commands.Extinguish()}
|
||||
command(ALICE_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
command(MEGA_CORP_PUBKEY) {Cash.Commands.Move()}
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the bill of lading has not been transferred"
|
||||
}
|
||||
|
||||
/* transaction {
|
||||
input { LOCstate.copy(issued = true, props = pros.copy(issueDate = LocalDate.now().minusDays(32))) }
|
||||
input { Billstate }
|
||||
input { Cashstate }
|
||||
input { invoiceState }
|
||||
output { LOCstate.copy(beneficiaryPaid = true, issued = true, props = pros.copy(issueDate =LocalDate.now().minusDays(32)))}
|
||||
output { Billstate.copy(beneficiary = CHARLIE)}
|
||||
output { Cashstate.copy(owner = ALICE_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY, ALICE_PUBKEY) { LOC.Commands.DemandPresentation() }
|
||||
command(ALICE_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
command(ALICE_PUBKEY) { Invoice.Commands.Extinguish()}
|
||||
command(MEGA_CORP_PUBKEY) {Cash.Commands.Move()}
|
||||
timestamp(Instant.now())
|
||||
this.`fails requirement`("the presentation is late");
|
||||
}*/
|
||||
|
||||
transaction {
|
||||
input { LOCstate.copy(issued = true) }
|
||||
input { Billstate }
|
||||
input { Cashstate }
|
||||
input { invoiceState }
|
||||
output { LOCstate.copy(beneficiaryPaid = false, issued = true)}
|
||||
output { Billstate.copy(beneficiary = CHARLIE)}
|
||||
output { Cashstate.copy(owner = ALICE_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY, ALICE_PUBKEY) { LOC.Commands.DemandPresentation() }
|
||||
command(ALICE_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
command(ALICE_PUBKEY) { Invoice.Commands.Extinguish()}
|
||||
command(MEGA_CORP_PUBKEY) {Cash.Commands.Move()}
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the beneficiary has not been paid, status not changed"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { LOCstate.copy(issued = false) }
|
||||
input { Billstate }
|
||||
input { Cashstate }
|
||||
input { invoiceState }
|
||||
output { LOCstate.copy(beneficiaryPaid = true, issued = false)}
|
||||
output { Billstate.copy(beneficiary = CHARLIE)}
|
||||
output { Cashstate.copy(owner = ALICE_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY, ALICE_PUBKEY) { LOC.Commands.DemandPresentation() }
|
||||
command(ALICE_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
command(ALICE_PUBKEY) { Invoice.Commands.Extinguish()}
|
||||
command(MEGA_CORP_PUBKEY) {Cash.Commands.Move()}
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the LOC must be Issued"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { LOCstate.copy(issued = true) }
|
||||
input { Billstate }
|
||||
input { Cashstate }
|
||||
input { invoiceState }
|
||||
output { LOCstate.copy(beneficiaryPaid = true, issued = true, terminated = true)}
|
||||
output { Billstate.copy(beneficiary = CHARLIE)}
|
||||
output { Cashstate.copy(owner = ALICE_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY, ALICE_PUBKEY) { LOC.Commands.DemandPresentation() }
|
||||
command(ALICE_PUBKEY) { BillOfLadingAgreement.Commands.TransferAndEndorseBL() }
|
||||
command(ALICE_PUBKEY) { Invoice.Commands.Extinguish()}
|
||||
command(MEGA_CORP_PUBKEY) {Cash.Commands.Move()}
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "LOC must not be terminated"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun terminationTests() {
|
||||
|
||||
transaction {
|
||||
input { LOCstate.copy(issued = true, beneficiaryPaid = true) }
|
||||
input { Cashstate.copy(owner = CHARLIE_PUBKEY) }
|
||||
output { LOCstate.copy(beneficiaryPaid = true, issued = true, terminated = true)}
|
||||
output { Cashstate.copy(owner = MEGA_CORP_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY, CHARLIE_PUBKEY) { LOC.Commands.Termination() }
|
||||
command(CHARLIE_PUBKEY) {Cash.Commands.Move()}
|
||||
timestamp(Instant.now())
|
||||
this.verifies()
|
||||
}
|
||||
transaction {
|
||||
input { LOCstate.copy(issued = true, beneficiaryPaid = true) }
|
||||
input { Cashstate.copy(owner = CHARLIE_PUBKEY) }
|
||||
output { LOCstate.copy(beneficiaryPaid = true, issued = true, terminated = true)}
|
||||
output { Cashstate.copy(owner = MEGA_CORP_PUBKEY) }
|
||||
command(ALICE_PUBKEY, CHARLIE_PUBKEY) { LOC.Commands.Termination() }
|
||||
command(CHARLIE_PUBKEY) {Cash.Commands.Move()}
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the transaction is signed by the issuing bank"
|
||||
}
|
||||
|
||||
/*transaction {
|
||||
input { LOCstate.copy(issued = true, beneficiaryPaid = true) }
|
||||
input { Cashstate.copy(owner = CHARLIE_PUBKEY) }
|
||||
output { LOCstate.copy(beneficiaryPaid = true, issued = true, terminated = true)}
|
||||
output { Cashstate.copy(owner = MEGA_CORP_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY, ALICE_PUBKEY) { LOC.Commands.Termination() }
|
||||
command(CHARLIE_PUBKEY) {Cash.Commands.Move()}
|
||||
timestamp(Instant.now())
|
||||
this.`fails requirement`("the transaction is signed by the applicant");
|
||||
}*/
|
||||
|
||||
transaction {
|
||||
input { LOCstate.copy(issued = true, beneficiaryPaid = true) }
|
||||
input { Cashstate.copy(owner = CHARLIE_PUBKEY) }
|
||||
output { LOCstate.copy(beneficiaryPaid = true, issued = true, terminated = true)}
|
||||
output { Cashstate.copy(amount = Cashstate.amount.minus(Amount(10,Cashstate.amount.token))) }
|
||||
command(MEGA_CORP_PUBKEY, CHARLIE_PUBKEY) { LOC.Commands.Termination() }
|
||||
command(CHARLIE_PUBKEY) {Cash.Commands.Move()}
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the cash state has not been transferred"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { LOCstate.copy(issued = true, beneficiaryPaid = true) }
|
||||
input { Cashstate.copy(owner = CHARLIE_PUBKEY) }
|
||||
output { LOCstate.copy(beneficiaryPaid = true, issued = true, terminated = true)}
|
||||
output { Cashstate.copy(owner = CHARLIE_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY, CHARLIE_PUBKEY) { LOC.Commands.Termination() }
|
||||
command(CHARLIE_PUBKEY) {Cash.Commands.Move()}
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "Empty collection can't be reduced"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { LOCstate.copy(issued = true, beneficiaryPaid = false) }
|
||||
input { Cashstate.copy(owner = CHARLIE_PUBKEY) }
|
||||
output { LOCstate.copy(beneficiaryPaid = false, issued = true, terminated = true)}
|
||||
output { Cashstate.copy(owner = MEGA_CORP_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY, CHARLIE_PUBKEY) { LOC.Commands.Termination() }
|
||||
command(CHARLIE_PUBKEY) {Cash.Commands.Move()}
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the beneficiary has not been paid, status not changed"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { LOCstate.copy(issued = false, beneficiaryPaid = true) }
|
||||
input { Cashstate.copy(owner = CHARLIE_PUBKEY) }
|
||||
output { LOCstate.copy(beneficiaryPaid = true, issued = false, terminated = true)}
|
||||
output { Cashstate.copy(owner = MEGA_CORP_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY, CHARLIE_PUBKEY) { LOC.Commands.Termination() }
|
||||
command(CHARLIE_PUBKEY) {Cash.Commands.Move()}
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the LOC must be Issued"
|
||||
}
|
||||
transaction {
|
||||
input { LOCstate.copy(issued = true, beneficiaryPaid = true) }
|
||||
input { Cashstate.copy(owner = CHARLIE_PUBKEY) }
|
||||
output { LOCstate.copy(beneficiaryPaid = true, issued = true, terminated = false)}
|
||||
output { Cashstate.copy(owner = MEGA_CORP_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY, CHARLIE_PUBKEY) { LOC.Commands.Termination() }
|
||||
command(CHARLIE_PUBKEY) {Cash.Commands.Move()}
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "LOC should be terminated"
|
||||
}
|
||||
|
||||
transaction {
|
||||
input { LOCstate.copy(issued = true, beneficiaryPaid = true, props = pros.copy(amount = 1.POUNDS `issued by` defaultIssuer)) }
|
||||
input { Cashstate.copy(owner = CHARLIE_PUBKEY) }
|
||||
output { LOCstate.copy(beneficiaryPaid = true, issued = true, terminated = true)}
|
||||
output { Cashstate.copy(owner = MEGA_CORP_PUBKEY) }
|
||||
command(MEGA_CORP_PUBKEY, CHARLIE_PUBKEY) { LOC.Commands.Termination() }
|
||||
command(CHARLIE_PUBKEY) {Cash.Commands.Move()}
|
||||
timestamp(Instant.now())
|
||||
this `fails with` "the LOC properties do not remain the same"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user