diff --git a/apps/flipcash/core/src/androidMain/res/values/strings.xml b/apps/flipcash/core/src/androidMain/res/values/strings.xml
index 8d17b0e5a3..5215446607 100644
--- a/apps/flipcash/core/src/androidMain/res/values/strings.xml
+++ b/apps/flipcash/core/src/androidMain/res/values/strings.xml
@@ -1988,10 +1988,6 @@
Select People
Members
-
- - Send to %1$d Person
- - Send to %1$d People
-
10 People Max
The maximum number of people you can send to at once is 10 people
diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt
index 4680aa9b02..c7b8ff0be1 100644
--- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt
+++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt
@@ -490,10 +490,12 @@ private fun FlowGroupSelectPeople(viewModel: ChatViewModel, onSendComplete: () -
val candidates = state.groupSendCandidates
GroupSelectPeopleContent(
candidates = candidates,
- selected = groupSend.selected,
+ selected = groupSend.checked,
onBack = { viewModel.dispatchEvent(ChatViewModel.Event.OnGroupSendBack) },
onToggle = { viewModel.dispatchEvent(ChatViewModel.Event.OnGroupSendMemberToggled(it.userId)) },
onSend = { viewModel.dispatchEvent(ChatViewModel.Event.OnGroupSendPeopleConfirmed) },
+ buttonEnabled = groupSend.selected.isNotEmpty(),
+ viewerId = groupSend.viewerId,
)
if (groupSend.stage == GroupSendState.Stage.Confirm) {
GroupSendConfirmSheet(
diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt
index 67cd3d42ca..36b28734f7 100644
--- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt
+++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt
@@ -682,7 +682,7 @@ internal class ChatViewModel @AssistedInject constructor(
val sendsToMembers: Boolean
get() = chatType == ChatType.GROUP && (subject as? ChatSubject.Group)?.isPrivate == true
- /** Who Select People lists: the roster, or what this device knows of it, less the viewer. */
+ /** Who Select People lists: the roster, or what this device knows of it, the viewer first. */
val groupSendCandidates: List
get() = groupSend?.candidates(members.ifEmpty { knownMembers }).orEmpty()
@@ -4056,15 +4056,15 @@ internal class ChatViewModel @AssistedInject constructor(
eventFlow.filterIsInstance()
.onEach { (member) ->
val groupSend = stateFlow.value.groupSend ?: return@onEach
- val selected = groupSend.toggled(member)
- if (selected == null) {
+ val toggled = groupSend.toggled(member)
+ if (toggled == null) {
BottomBarManager.showInfo(
title = resources.getString(R.string.title_groupSendMaxPeople),
message = resources.getString(R.string.description_groupSendMaxPeople),
)
return@onEach
}
- update { it.copy(selected = selected) }
+ update { toggled }
}.launchIn(viewModelScope)
eventFlow.filterIsInstance()
@@ -4088,8 +4088,8 @@ internal class ChatViewModel @AssistedInject constructor(
title = resources.getString(R.string.error_title_groupSendTooSmall),
message = resources.getQuantityString(
R.plurals.error_description_groupSendTooSmall,
- groupSend.selected.size,
- groupSend.selected.size,
+ groupSend.shares,
+ groupSend.shares,
),
)
GroupSendPlan.NoRecipients, GroupSendPlan.TooManyRecipients -> Unit
diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/groupsend/GroupSendState.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/groupsend/GroupSendState.kt
index 1785a6ce33..ec8970950a 100644
--- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/groupsend/GroupSendState.kt
+++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/groupsend/GroupSendState.kt
@@ -11,31 +11,47 @@ import com.getcode.opencode.model.financial.Fiat
* swipe. Node 11293:6918.
*
* @param total what the sender entered, in the currency it was entered in. Null until then.
- * @param selected who to pay, in the order they were checked: the order decides the split.
+ * @param selected who to pay, in the order they were checked: the order decides the split. Never
+ * the sender, who is [includesSender] instead.
+ * @param includesSender whether the sender checked "You", splitting [total] with themselves and
+ * keeping their part.
*/
internal data class GroupSendState(
val viewerId: ID?,
val stage: Stage = Stage.Amount,
val total: Fiat? = null,
val selected: List = emptyList(),
+ val includesSender: Boolean = false,
) {
enum class Stage { Amount, People, Confirm }
/** The parts to pay, once [total] is known and someone is checked. */
val plan: GroupSendPlan
- get() = total?.let { GroupSendSplit.split(GroupSendSplit.minorUnits(it), selected.size) }
+ get() = total?.let { GroupSendSplit.split(GroupSendSplit.minorUnits(it), selected.size, includesSender) }
?: GroupSendPlan.NoRecipients
- /** Who Select People lists: the roster without the sender, who never pays themselves. */
+ /** How many ways [total] splits: [selected], and the sender when [includesSender]. */
+ val shares: Int get() = selected.size + if (includesSender) 1 else 0
+
+ /** The rows Select People shows checked: [selected], and the sender's when [includesSender]. */
+ val checked: List get() = if (includesSender) selected + listOfNotNull(viewerId) else selected
+
+ /** Who Select People lists: the sender first, as "You", then the rest of the roster. */
fun candidates(roster: List): List =
- roster.filter { it.userId != viewerId }.distinctBy { it.userId }
+ roster.distinctBy { it.userId }.sortedByDescending { it.userId == viewerId }
/**
- * [selected] with [member] checked or unchecked, or null when checking them would make an 11th.
+ * This state with [member] checked or unchecked, the sender included, or null when checking
+ * them would make an 11th share.
*/
- fun toggled(member: ID): List? = when {
- member in selected -> selected.filterNot { it == member }
- selected.size >= GroupSendSplit.MAX_RECIPIENTS -> null
- else -> selected.plusElement(member)
+ fun toggled(member: ID): GroupSendState? = when {
+ member == viewerId -> when {
+ includesSender -> copy(includesSender = false)
+ shares >= GroupSendSplit.MAX_RECIPIENTS -> null
+ else -> copy(includesSender = true)
+ }
+ member in selected -> copy(selected = selected.filterNot { it == member })
+ shares >= GroupSendSplit.MAX_RECIPIENTS -> null
+ else -> copy(selected = selected.plusElement(member))
}
}
diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/cash/GroupSendScreens.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/cash/GroupSendScreens.kt
index 9b42cf5fc6..f588edf462 100644
--- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/cash/GroupSendScreens.kt
+++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/cash/GroupSendScreens.kt
@@ -65,8 +65,9 @@ import com.getcode.ui.theme.CodeScaffold
import com.getcode.view.LoadingSuccessState
/**
- * Select People, node 11293:6918: the group's members less the sender, checked one by one in the
- * order that decides the split. There is no Select All.
+ * Select People, node 11293:6918: the group's members, checked one by one in the order that decides
+ * the split. There is no Select All; the sender's "You" row takes its place, as the first of
+ * [candidates] when [viewerId] is set.
*/
@Composable
internal fun GroupSelectPeopleContent(
@@ -75,7 +76,7 @@ internal fun GroupSelectPeopleContent(
onBack: () -> Unit,
onToggle: (ChatMember) -> Unit,
onSend: () -> Unit,
- buttonText: String = pluralStringResource(R.plurals.action_sendToPeople, selected.size, selected.size),
+ buttonText: String = stringResource(R.string.action_next),
buttonEnabled: Boolean = selected.isNotEmpty(),
/** Shown as "You" rather than by name, for a list the viewer is on. */
viewerId: ID? = null,
@@ -196,8 +197,9 @@ internal fun GroupSendConfirmSheet(
swipePay: SwipePayUiState,
actions: SwipePayActions,
) {
+ // The total entered, not what is sent: with You checked, the sender's part stays behind.
val total = groupSend.total ?: return
- val parts = (groupSend.plan as? GroupSendPlan.Pay)?.parts ?: return
+ val plan = groupSend.plan as? GroupSendPlan.Pay ?: return
val unknownName = stringResource(R.string.title_unnamedUser)
SwipePaySheet(
@@ -226,9 +228,9 @@ internal fun GroupSendConfirmSheet(
style = CodeTheme.typography.displayMedium,
color = CodeTheme.colors.textMain,
)
- if (parts.size >= 2) {
+ if (plan.shares.size >= 2) {
Text(
- text = eachLabel(parts, total.currencyCode),
+ text = eachLabel(plan, total.currencyCode),
style = CodeTheme.typography.textSmall,
color = CodeTheme.colors.textSecondary,
)
@@ -238,11 +240,14 @@ internal fun GroupSendConfirmSheet(
)
}
-/** "$1.00 Each" when the parts are equal, else "~$0.33 Each" for the smallest. */
+/**
+ * "$1.00 Each" when the shares are equal, else "~$0.33 Each" for the smallest. The sender's kept
+ * share counts: You + 1 on $5.00 is "$2.50 Each".
+ */
@Composable
-private fun eachLabel(parts: List, currency: CurrencyCode): String {
- val smallest = GroupSendSplit.fiat(parts.min(), currency).formatted()
- return if (parts.distinct().size == 1) stringResource(R.string.label_groupSendEach, smallest)
+private fun eachLabel(plan: GroupSendPlan.Pay, currency: CurrencyCode): String {
+ val smallest = GroupSendSplit.fiat(plan.smallest, currency).formatted()
+ return if (plan.isEven) stringResource(R.string.label_groupSendEach, smallest)
else stringResource(R.string.label_groupSendEachApprox, smallest)
}
diff --git a/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatGroupCashLinkTest.kt b/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatGroupCashLinkTest.kt
index 0fa10d4bf2..e10572c8c7 100644
--- a/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatGroupCashLinkTest.kt
+++ b/apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatGroupCashLinkTest.kt
@@ -382,7 +382,7 @@ class ChatGroupCashLinkTest {
}
@Test
- fun `a private group's amount moves on to Select People without the viewer`() = runTest(mainCoroutineRule.dispatcher) {
+ fun `a private group's amount moves on to Select People with the viewer first`() = runTest(mainCoroutineRule.dispatcher) {
val vm = createViewModel()
selectingPeople(vm, total = 2.0)
advanceUntilIdle()
@@ -390,7 +390,8 @@ class ChatGroupCashLinkTest {
val state = vm.stateFlow.value
assertEquals(GroupSendState.Stage.People, state.groupSend?.stage)
assertEquals(Fiat(2.0), state.groupSend?.total)
- assertEquals(roster.map { it.userId }, state.groupSendCandidates.map { it.userId })
+ assertEquals(listOf(viewerId) + roster.map { it.userId }, state.groupSendCandidates.map { it.userId })
+ assertEquals(false, state.groupSend?.includesSender)
coVerify(exactly = 0) { chatCashLinks.sendToChat(any(), any(), any(), any()) }
}
@@ -444,4 +445,52 @@ class ChatGroupCashLinkTest {
coVerify(exactly = 0) { chatCashLinks.sendToChat(any(), any(), any(), any()) }
assertTrue(events.any { it is ChatViewModel.Event.SendComplete })
}
+
+ @Test
+ fun `checking You splits with the sender, who keeps the last share`() = runTest(mainCoroutineRule.dispatcher) {
+ val vm = createViewModel()
+ selectingPeople(vm, total = 5.0)
+ vm.dispatchEvent(ChatViewModel.Event.OnGroupSendMemberToggled(viewerId))
+ vm.check(4)
+ vm.dispatchEvent(ChatViewModel.Event.OnGroupSendPeopleConfirmed)
+ vm.dispatchEvent(ChatViewModel.Event.OnGroupSendRequested)
+ advanceUntilIdle()
+
+ coVerify(exactly = 1) {
+ groupSendPayments.send(
+ chatId = chatId,
+ recipients = roster.take(4).map { it.userId },
+ plan = GroupSendPlan.Pay(listOf(100, 100, 100, 100), kept = 100),
+ currency = CurrencyCode.USD,
+ token = token,
+ source = any(),
+ )
+ }
+ }
+
+ @Test
+ fun `You counts toward the ten`() = runTest(mainCoroutineRule.dispatcher) {
+ val vm = createViewModel()
+ selectingPeople(vm, total = 20.0)
+ vm.dispatchEvent(ChatViewModel.Event.OnGroupSendMemberToggled(viewerId))
+ vm.check(10)
+ advanceUntilIdle()
+
+ val groupSend = vm.stateFlow.value.groupSend
+ assertEquals(roster.take(9).map { it.userId }, groupSend?.selected)
+ assertEquals(true, groupSend?.includesSender)
+ assertEquals(1, BottomBarManager.messages.value.size)
+ }
+
+ @Test
+ fun `You alone sends nothing`() = runTest(mainCoroutineRule.dispatcher) {
+ val vm = createViewModel()
+ selectingPeople(vm, total = 5.0)
+ vm.dispatchEvent(ChatViewModel.Event.OnGroupSendMemberToggled(viewerId))
+ vm.dispatchEvent(ChatViewModel.Event.OnGroupSendPeopleConfirmed)
+ advanceUntilIdle()
+
+ assertEquals(GroupSendState.Stage.People, vm.stateFlow.value.groupSend?.stage)
+ assertEquals(emptyList(), vm.stateFlow.value.groupSend?.selected)
+ }
}
diff --git a/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/shared/payments/group/GroupSendSplit.kt b/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/shared/payments/group/GroupSendSplit.kt
index bb556126a6..29190c2d10 100644
--- a/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/shared/payments/group/GroupSendSplit.kt
+++ b/apps/flipcash/shared/payments/src/main/kotlin/com/flipcash/shared/payments/group/GroupSendSplit.kt
@@ -11,20 +11,29 @@ sealed interface GroupSendPlan {
/**
* Pay [parts], in the currency's minor unit and in selection order. One part is a single
* payment; two or more are one multi-payment.
+ *
+ * @param kept the sender's own part when they split the total with themselves: the last share,
+ * never sent. Null when they didn't.
*/
- data class Pay(val parts: List) : GroupSendPlan {
- val isEven: Boolean get() = parts.all { it == parts.first() }
+ data class Pay(val parts: List, val kept: Long? = null) : GroupSendPlan {
+ /** Every part of the total, the sender's included: what "Each" and its "~" describe. */
+ val shares: List get() = parts + listOfNotNull(kept)
+
+ /** What the intent pays: the total less [kept]. */
+ val sent: Long get() = parts.sum()
+
+ val isEven: Boolean get() = shares.all { it == shares.first() }
/** The part shown as "Each": every part when [isEven], the smallest otherwise. */
- val smallest: Long get() = parts.min()
+ val smallest: Long get() = shares.min()
}
data object NoRecipients : GroupSendPlan
- /** More than [GroupSendSplit.MAX_RECIPIENTS] were selected. */
+ /** More than [GroupSendSplit.MAX_RECIPIENTS] shares, the sender's included, were selected. */
data object TooManyRecipients : GroupSendPlan
- /** The total is under [GroupSendSplit.MINIMUM_PART_UNITS] per recipient. */
+ /** The total is under [GroupSendSplit.MINIMUM_PART_UNITS] per share, the sender's included. */
data object PartBelowMinimum : GroupSendPlan
}
@@ -36,18 +45,30 @@ sealed interface GroupSendPlan {
* part is the total divided by the recipient count, rounded down to that unit, and the leftover
* goes one unit each to the first recipients in selection order, so the parts add up to the total
* exactly.
+ *
+ * A sender who includes themselves takes the last share, so they never take a leftover unit, and
+ * keep it: the parts paid are the shares before it. The cap and the minimum count their share.
*/
object GroupSendSplit {
const val MAX_RECIPIENTS = 10
const val MINIMUM_PART_UNITS = 1L
- fun split(totalUnits: Long, recipients: Int): GroupSendPlan {
+ /**
+ * @param recipients the members paid, not counting the sender.
+ * @param includesSender whether the sender takes a share of [totalUnits] and keeps it.
+ */
+ fun split(totalUnits: Long, recipients: Int, includesSender: Boolean = false): GroupSendPlan {
if (recipients <= 0) return GroupSendPlan.NoRecipients
- if (recipients > MAX_RECIPIENTS) return GroupSendPlan.TooManyRecipients
- val base = totalUnits / recipients
+ val shares = if (includesSender) recipients + 1 else recipients
+ if (shares > MAX_RECIPIENTS) return GroupSendPlan.TooManyRecipients
+ val base = totalUnits / shares
if (base < MINIMUM_PART_UNITS) return GroupSendPlan.PartBelowMinimum
- val leftover = totalUnits % recipients
- return GroupSendPlan.Pay(List(recipients) { i -> if (i < leftover) base + 1 else base })
+ val leftover = totalUnits % shares
+ return GroupSendPlan.Pay(
+ parts = List(recipients) { i -> if (i < leftover) base + 1 else base },
+ // The last share: the leftover never reaches it, since leftover < shares.
+ kept = base.takeIf { includesSender },
+ )
}
/** [fiat] in its currency's minor unit, rounded half-up to that unit. */
diff --git a/apps/flipcash/shared/payments/src/test/kotlin/com/flipcash/shared/payments/group/GroupSendSplitVectorsTest.kt b/apps/flipcash/shared/payments/src/test/kotlin/com/flipcash/shared/payments/group/GroupSendSplitVectorsTest.kt
index 2f0c5404a4..b5bfb8e434 100644
--- a/apps/flipcash/shared/payments/src/test/kotlin/com/flipcash/shared/payments/group/GroupSendSplitVectorsTest.kt
+++ b/apps/flipcash/shared/payments/src/test/kotlin/com/flipcash/shared/payments/group/GroupSendSplitVectorsTest.kt
@@ -28,11 +28,12 @@ class GroupSendSplitVectorsTest {
@Test
fun `every vector`() {
val vectors = fixture.getJSONArray("vectors")
- assertEquals(14, vectors.length())
+ assertEquals(23, vectors.length())
for (i in 0 until vectors.length()) {
val v = vectors.getJSONObject(i)
val name = v.getString("name")
- val plan = GroupSendSplit.split(cents(v.getString("total")), v.getInt("recipients"))
+ val includesSender = v.optBoolean("includesSender", false)
+ val plan = GroupSendSplit.split(cents(v.getString("total")), v.getInt("recipients"), includesSender)
if (v.has("expectedError")) {
val error = when (val rule = v.getString("expectedError")) {
@@ -51,14 +52,28 @@ class GroupSendSplitVectorsTest {
val parts = v.getJSONArray("parts")
assertEquals((0 until parts.length()).map { cents(parts.getString(it)) }, pay.parts, name)
+ assertEquals(v.optString("kept").ifEmpty { null }?.let(::cents), pay.kept, name)
+ if (includesSender) assertEquals(cents(v.getString("sent")), pay.sent, name)
- if (v.has("eachLabel")) {
- val each = "$" + BigDecimal.valueOf(pay.smallest, 2).toPlainString() + " Each"
- assertEquals(v.getString("eachLabel"), if (pay.isEven) each else "~$each", name)
- }
+ assertEquals(v.optString("eachLabel").ifEmpty { null }, pay.takeIf { it.shares.size > 1 }?.let(::eachLabel), name)
}
}
+ // Not in the fixture: the paid parts are equal but the sender's is a cent less, so "~" must
+ // come from the sender's share. 5 cents between You and 2 pays 2 and 2 and keeps 1.
+ @Test
+ fun `the sender's smaller share makes the label approximate`() {
+ val pay = assertIs(GroupSendSplit.split(5, 2, includesSender = true))
+ assertEquals(listOf(2L, 2L), pay.parts)
+ assertEquals(1L, pay.kept)
+ assertEquals("~$0.01 Each", eachLabel(pay))
+ }
+
+ private fun eachLabel(pay: GroupSendPlan.Pay): String {
+ val each = "$" + BigDecimal.valueOf(pay.smallest, 2).toPlainString() + " Each"
+ return if (pay.isEven) each else "~$each"
+ }
+
@Test
fun `minor units round trip through Fiat`() {
assertEquals(12345L, GroupSendSplit.minorUnits(Fiat("123.45", CurrencyCode.USD)))
diff --git a/test-vectors/group-send-split.json b/test-vectors/group-send-split.json
index 00685790ca..748516c718 100644
--- a/test-vectors/group-send-split.json
+++ b/test-vectors/group-send-split.json
@@ -1,7 +1,7 @@
{
"algorithm": "group-send-split",
"source": "docs/specs/group-send.md (Splitting the total); flipcash2-client-protocol 0.21.0 CashContent.multi_receiver; ocp SendPublicMultiPaymentMetadata",
- "note": "Amounts are USD decimal strings. parts are in selection order: the first recipients checked get the leftover cents. Quarks are omitted: each part is priced from its own fiat at one verified state. expectedError names the rule, not a platform error type.",
+ "note": "Amounts are USD decimal strings. parts are in selection order: the first recipients checked get the leftover cents. Quarks are omitted: each part is priced from its own fiat at one verified state. expectedError names the rule, not a platform error type. With includesSender, the sender's part is the last share, kept rather than paid: parts lists the paid members only, kept is the sender's part, sent is what the intent pays, and maxRecipients counts the sender's share.",
"maxRecipients": 10,
"minimumPart": "0.01",
"vectors": [
@@ -176,6 +176,137 @@
"total": "1.00",
"recipients": 0,
"expectedError": "no-recipients"
+ },
+ {
+ "name": "you and four",
+ "total": "5.00",
+ "recipients": 4,
+ "includesSender": true,
+ "intent": "SendPublicMultiPayment",
+ "parts": [
+ "1.00",
+ "1.00",
+ "1.00",
+ "1.00"
+ ],
+ "kept": "1.00",
+ "sent": "4.00",
+ "eachLabel": "$1.00 Each"
+ },
+ {
+ "name": "you and one",
+ "total": "5.00",
+ "recipients": 1,
+ "includesSender": true,
+ "intent": "SendPublicPayment",
+ "parts": [
+ "2.50"
+ ],
+ "kept": "2.50",
+ "sent": "2.50",
+ "eachLabel": "$2.50 Each"
+ },
+ {
+ "name": "you and two uneven",
+ "total": "1.00",
+ "recipients": 2,
+ "includesSender": true,
+ "intent": "SendPublicMultiPayment",
+ "parts": [
+ "0.34",
+ "0.33"
+ ],
+ "kept": "0.33",
+ "sent": "0.67",
+ "eachLabel": "~$0.33 Each"
+ },
+ {
+ "name": "you and three one cent left",
+ "total": "1.01",
+ "recipients": 3,
+ "includesSender": true,
+ "intent": "SendPublicMultiPayment",
+ "parts": [
+ "0.26",
+ "0.25",
+ "0.25"
+ ],
+ "kept": "0.25",
+ "sent": "0.76",
+ "eachLabel": "~$0.25 Each"
+ },
+ {
+ "name": "you and two equal parts, less kept",
+ "total": "0.05",
+ "recipients": 2,
+ "includesSender": true,
+ "intent": "SendPublicMultiPayment",
+ "parts": [
+ "0.02",
+ "0.02"
+ ],
+ "kept": "0.01",
+ "sent": "0.04",
+ "eachLabel": "~$0.01 Each"
+ },
+ {
+ "name": "you and five exact minimum",
+ "total": "0.06",
+ "recipients": 5,
+ "includesSender": true,
+ "intent": "SendPublicMultiPayment",
+ "parts": [
+ "0.01",
+ "0.01",
+ "0.01",
+ "0.01",
+ "0.01"
+ ],
+ "kept": "0.01",
+ "sent": "0.05",
+ "eachLabel": "$0.01 Each"
+ },
+ {
+ "name": "you and nine",
+ "total": "10.00",
+ "recipients": 9,
+ "includesSender": true,
+ "intent": "SendPublicMultiPayment",
+ "parts": [
+ "1.00",
+ "1.00",
+ "1.00",
+ "1.00",
+ "1.00",
+ "1.00",
+ "1.00",
+ "1.00",
+ "1.00"
+ ],
+ "kept": "1.00",
+ "sent": "9.00",
+ "eachLabel": "$1.00 Each"
+ },
+ {
+ "name": "you and ten",
+ "total": "11.00",
+ "recipients": 10,
+ "includesSender": true,
+ "expectedError": "too-many-recipients"
+ },
+ {
+ "name": "too small with you",
+ "total": "0.02",
+ "recipients": 2,
+ "includesSender": true,
+ "expectedError": "part-below-minimum"
+ },
+ {
+ "name": "you alone",
+ "total": "1.00",
+ "recipients": 0,
+ "includesSender": true,
+ "expectedError": "no-recipients"
}
]
}