Add accepted/denied payment methods to place details #102

Merged
raucao merged 3 commits from feature/payment_details into master 2026-09-08 21:36:12 +00:00
11 changed files with 804 additions and 1 deletions
+20 -1
View File
@@ -2,6 +2,16 @@ import Component from '@glimmer/component';
import { htmlSafe } from '@ember/template';
import { getIcon, isIconFilled } from '../utils/icons';
function formatDimension(dim) {
if (typeof dim === 'number') {
return `${dim}px`;
}
if (typeof dim === 'string' && /^\d+(\.\d+)?$/.test(dim.trim())) {
return `${dim.trim()}px`;
}
return dim;
}
export default class IconComponent extends Component {
get svg() {
return getIcon(this.args.name);
@@ -11,13 +21,21 @@ export default class IconComponent extends Component {
return this.args.size || 16;
}
get width() {
return this.args.width !== undefined ? this.args.width : this.size;
}
get height() {
return this.args.height !== undefined ? this.args.height : this.size;
}
get color() {
return this.args.color || '#898989';
}
get style() {
return htmlSafe(
`width:${this.size}px;height:${this.size}px;color:${this.color}`
`width:${formatDimension(this.width)};height:${formatDimension(this.height)};color:${this.color}`
);
}
@@ -35,6 +53,7 @@ export default class IconComponent extends Component {
class="icon {{if this.isFilled 'icon-filled'}}"
style={{this.style}}
title={{this.title}}
...attributes
>
{{htmlSafe this.svg}}
</span>
+3
View File
@@ -15,6 +15,7 @@ import NostrConnect from './nostr-connect';
import Modal from './modal';
import PhotoCarousel from './photo-carousel';
import PhotoGallery from './photo-gallery';
import PlacePaymentMethods from './place-payment-methods';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
@@ -467,6 +468,8 @@ export default class PlaceDetails extends Component {
{{/if}}
</div>
<PlacePaymentMethods @tags={{this.tags}} />
<div class="meta-info">
{{#if this.cuisine}}
+93
View File
@@ -0,0 +1,93 @@
import Component from '@glimmer/component';
import Icon from './icon';
import { parsePaymentMethods } from '../utils/payment';
import tooltip from '../modifiers/tooltip';
export default class PlacePaymentMethods extends Component {
get payment() {
return parsePaymentMethods(this.args.tags);
}
get methods() {
const list = [];
if (this.payment.cash !== null) {
const isDenied = this.payment.cash === 'denied';
list.push({
id: 'cash',
isDenied,
icon: 'banknote',
color: 'currentColor',
description: isDenied ? 'No cash' : 'Cash accepted',
hasBadge: false,
});
}
if (this.payment.cards !== null) {
const isDenied = this.payment.cards === 'denied';
list.push({
id: 'cards',
isDenied,
icon: 'payment-card',
color: 'currentColor',
description: isDenied ? 'No cards' : 'Cards accepted',
hasBadge: false,
});
}
if (this.payment.bitcoin.status !== null) {
const isDenied = this.payment.bitcoin.status === 'denied';
let description = 'No Bitcoin';
if (!isDenied) {
description = this.payment.bitcoin.lightning
? 'Bitcoin (Lightning) accepted'
: 'Bitcoin (On-chain) accepted';
}
list.push({
id: 'bitcoin',
isDenied,
icon: 'bitcoin',
width: 17,
height: 22,
color: 'currentColor',
description,
hasBadge: !isDenied && this.payment.bitcoin.lightning,
});
}
return list;
}
<template>
{{! template-lint-disable no-unsupported-role-attributes }}
{{#if this.payment.hasPaymentInfo}}
<div class="place-payment-methods" aria-label="Payment methods">
{{#each this.methods as |method|}}
<span
class="payment-method {{if method.isDenied 'is-denied'}}"
data-test-payment-method={{method.id}}
aria-label={{method.description}}
aria-description={{method.description}}
tabindex="0"
{{tooltip}}
>
<Icon
@name={{method.icon}}
@size={{22}}
@width={{method.width}}
@height={{method.height}}
@color={{method.color}}
/>
{{#if method.hasBadge}}
<span
class="payment-method-badge"
data-test-payment-badge="lightning"
>
<Icon @name="zap" @size={{13}} @color="#fff" @filled={{true}} />
</span>
{{/if}}
</span>
{{/each}}
</div>
{{/if}}
</template>
}
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="8.157 6 15.088 19.949">
<path d="M23.189 14.02c.314-2.096-1.283-3.223-3.465-3.975l.708-2.84-1.728-.43-.69 2.765c-.454-.114-.92-.22-1.385-.326l.695-2.783L15.596 6l-.708 2.839c-.376-.086-.746-.17-1.104-.26l.002-.009-2.384-.595-.46 1.846s1.283.294 1.256.312c.7.175.826.638.805 1.006l-.806 3.235c.048.012.11.03.18.057l-.183-.045-1.13 4.532c-.086.212-.303.531-.793.41.018.025-1.256-.313-1.256-.313l-.858 1.978 2.25.561c.418.105.828.215 1.231.318l-.715 2.872 1.727.43.708-2.84c.472.127.93.245 1.378.357l-.706 2.828 1.728.43.715-2.866c2.948.558 5.164.333 6.097-2.333.752-2.146-.037-3.385-1.588-4.192 1.13-.26 1.98-1.003 2.207-2.538zm-3.95 5.538c-.533 2.147-4.148.986-5.32.695l.95-3.805c1.172.293 4.929.872 4.37 3.11zm.535-5.569c-.487 1.953-3.495.96-4.47.717l.86-3.45c.975.243 4.118.696 3.61 2.733z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 872 B

+65
View File
@@ -1175,6 +1175,71 @@ abbr[title] {
margin-top: 1.5rem;
}
.place-payment-methods {
display: flex;
align-items: center;
gap: 1rem;
margin-top: 1.2rem;
padding-top: 1.2rem;
border-top: 1px solid var(--divider-color);
color: var(--secondary-text-color);
}
.payment-method {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
height: 24px;
border-radius: 4px;
outline-offset: 2px;
color: var(--secondary-text-color);
}
.payment-method.is-denied {
opacity: 0.5;
}
.payment-method.is-denied::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 28px;
height: 2px;
background-color: var(--danger-color);
transform: translate(-50%, -50%) rotate(-45deg);
pointer-events: none;
border-radius: 1px;
}
.payment-method-badge {
position: absolute;
bottom: -2px;
right: -6px;
display: flex;
align-items: center;
justify-content: center;
width: 13px;
height: 13px;
border-radius: 50%;
background: var(--default-list-color);
overflow: hidden;
pointer-events: none;
font-size: 0;
line-height: 0;
}
.payment-method-badge .icon {
display: flex;
align-items: center;
justify-content: center;
}
.payment-method-badge .icon svg {
display: block;
}
.btn {
/* TODO: remove this scoped rule in favor of a global box-sizing reset
(e.g. `*, *::before, *::after { box-sizing: border-box; }`) at the top
+7
View File
@@ -113,6 +113,7 @@ import molarTooth from '@waysidemapping/pinhead/dist/icons/molar_tooth.svg?raw';
import needleAndSpoolOfThread from '@waysidemapping/pinhead/dist/icons/needle_and_spool_of_thread.svg?raw';
import openBook from '@waysidemapping/pinhead/dist/icons/open_book.svg?raw';
import palace from '@waysidemapping/pinhead/dist/icons/palace.svg?raw';
import paymentCard from '@waysidemapping/pinhead/dist/icons/payment_card.svg?raw';
import parkingP from '@waysidemapping/pinhead/dist/icons/p_wide.svg?raw';
import personCricketBattingAtCricketBall from '@waysidemapping/pinhead/dist/icons/person_cricket_batting_at_cricket_ball.svg?raw';
import personBoardingTramWithDestinationDisplayAndPantographOnTramTrack from '@waysidemapping/pinhead/dist/icons/person_boarding_tram_with_destination_display_and_pantograph_on_tram_track.svg?raw';
@@ -148,6 +149,7 @@ import womensAndMensRestroomSymbol from '@waysidemapping/pinhead/dist/icons/wome
/*
* Custom/local icons
*/
import bitcoin from '../icons/bitcoin.svg?raw';
import loadingRing from '../icons/270-ring.svg?raw';
import nostrich from '../icons/nostrich-2.svg?raw';
import remotestorage from '../icons/remotestorage.svg?raw';
@@ -167,6 +169,7 @@ const ICONS = {
'badge-shield-with-fire': badgeShieldWithFire,
'beach-umbrella-in-ground': beachUmbrellaInGround,
'beer-mug-with-foam': beerMugWithFoam,
bitcoin,
bookmark,
'boxing-glove-up': boxingGloveUp,
'burger-and-drink-cup-with-straw': burgerAndDrinkCupWithStraw,
@@ -246,6 +249,7 @@ const ICONS = {
nostrich,
'open-book': openBook,
palace,
'payment-card': paymentCard,
'person-cricket-batting-at-cricket-ball': personCricketBattingAtCricketBall,
'person-boarding-tram-with-destination-display-and-pantograph-on-tram-track':
personBoardingTramWithDestinationDisplayAndPantographOnTramTrack,
@@ -303,7 +307,10 @@ const ICONS = {
};
const FILLED_ICONS = [
'banknote',
'bitcoin',
'fork-and-knife',
'payment-card',
'wikipedia',
'whatsapp',
'cup-and-saucer',
+111
View File
@@ -0,0 +1,111 @@
const CARD_KEYS = [
'payment:cards',
'payment:credit_cards',
'payment:debit_cards',
'payment:contactless',
'payment:visa',
'payment:mastercard',
'payment:american_express',
'payment:amex',
'payment:maestro',
'payment:girocard',
'payment:discover_card',
'payment:diners_club',
'payment:jcb',
'payment:unionpay',
'payment:bancontact',
'payment:postfinance_card',
'payment:mir',
'payment:dankort',
'payment:interac',
'payment:visa_debit',
'payment:mastercard_debit',
'payment:apple_pay',
'payment:google_pay',
];
function isYes(val) {
return val === 'yes' || val === 'only';
}
function isNo(val) {
return val === 'no';
}
export function parsePaymentMethods(tags = {}) {
const safeTags = tags || {};
// 1. Cash
let cash = null;
const cashVal = safeTags['payment:cash'];
const coinsVal = safeTags['payment:coins'];
const notesVal = safeTags['payment:notes'];
if (
isYes(cashVal) ||
(!isNo(cashVal) && (isYes(coinsVal) || isYes(notesVal)))
) {
cash = 'accepted';
} else if (
isNo(cashVal) ||
(!isYes(cashVal) && isNo(coinsVal) && isNo(notesVal))
) {
cash = 'denied';
}
// 2. Cards
let cards = null;
const anyCardAccepted = CARD_KEYS.some((key) => isYes(safeTags[key]));
if (anyCardAccepted) {
cards = 'accepted';
} else {
const cardsNo = isNo(safeTags['payment:cards']);
const creditAndDebitNo =
isNo(safeTags['payment:credit_cards']) &&
isNo(safeTags['payment:debit_cards']);
const cashOnly = safeTags['payment:cash'] === 'only';
if (cardsNo || creditAndDebitNo || cashOnly) {
cards = 'denied';
}
}
// 3. Bitcoin
const xbtVal = safeTags['currency:XBT'];
const lightningVal = safeTags['payment:lightning'];
const onchainVal = safeTags['payment:onchain'];
const btcVal = safeTags['payment:bitcoin'];
const btcCurrencyVal = safeTags['currency:BTC'];
const isBtcAccepted =
isYes(xbtVal) ||
isYes(lightningVal) ||
isYes(onchainVal) ||
isYes(btcVal) ||
isYes(btcCurrencyVal);
const isBtcDenied = !isBtcAccepted && (isNo(xbtVal) || isNo(btcVal));
const lightning = isYes(lightningVal);
const onchain =
isYes(onchainVal) ||
((isYes(xbtVal) || isYes(btcVal) || isYes(btcCurrencyVal)) &&
!isNo(onchainVal));
const bitcoin = {
status: isBtcAccepted ? 'accepted' : isBtcDenied ? 'denied' : null,
lightning,
onchain: isBtcAccepted ? onchain : false,
};
const hasPaymentInfo =
cash !== null || cards !== null || bitcoin.status !== null;
return {
hasPaymentInfo,
cash,
cards,
bitcoin,
};
}
@@ -0,0 +1,61 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'marco/tests/helpers';
import { render } from '@ember/test-helpers';
import Icon from 'marco/components/icon';
module('Integration | Component | icon', function (hooks) {
setupRenderingTest(hooks);
test('it renders default 16px square dimensions', async function (assert) {
await render(<template><Icon @name="zap" /></template>);
assert.dom('.icon').exists();
assert.dom('.icon').hasAttribute('style', /width:16px;height:16px;/);
});
test('it renders custom square size with @size', async function (assert) {
await render(<template><Icon @name="zap" @size={{24}} /></template>);
assert.dom('.icon').hasAttribute('style', /width:24px;height:24px;/);
});
test('it supports custom @width and @height', async function (assert) {
await render(
<template><Icon @name="bitcoin" @width={{17}} @height={{22}} /></template>
);
assert.dom('.icon').hasAttribute('style', /width:17px;height:22px;/);
});
test('it allows @width to override while @height falls back to @size', async function (assert) {
await render(
<template><Icon @name="bitcoin" @width={{17}} @size={{22}} /></template>
);
assert.dom('.icon').hasAttribute('style', /width:17px;height:22px;/);
});
test('it allows @height to override while @width falls back to @size', async function (assert) {
await render(
<template><Icon @name="bitcoin" @height={{22}} @size={{18}} /></template>
);
assert.dom('.icon').hasAttribute('style', /width:18px;height:22px;/);
});
test('it supports explicit string units for width and height', async function (assert) {
await render(
<template><Icon @name="zap" @width="100%" @height="2rem" /></template>
);
assert.dom('.icon').hasAttribute('style', /width:100%;height:2rem;/);
});
test('it splats HTML attributes to the icon element', async function (assert) {
await render(
<template><Icon @name="zap" data-test-custom-icon="true" /></template>
);
assert.dom('[data-test-custom-icon="true"]').exists();
});
});
@@ -411,4 +411,50 @@ module('Integration | Component | place-details', function (hooks) {
assert.dom('button.btn-link').doesNotExist();
});
test('it renders payment methods when payment tags are present on place', async function (assert) {
const place = {
title: 'Coffee Place',
lat: 52.52,
lon: 13.4,
osmTags: {
'payment:cash': 'yes',
'payment:cards': 'no',
'currency:XBT': 'yes',
'payment:lightning': 'yes',
},
};
await render(<template><PlaceDetails @place={{place}} /></template>);
assert.dom('.place-payment-methods').exists();
assert.dom('[data-test-payment-method="cash"]').exists();
assert.dom('[data-test-payment-method="cards"]').hasClass('is-denied');
assert.dom('[data-test-payment-method="bitcoin"]').exists();
assert.dom('[data-test-payment-badge="lightning"]').exists();
const methodNames = Array.from(
this.element.querySelectorAll('[data-test-payment-method]')
).map((el) => el.getAttribute('data-test-payment-method'));
assert.deepEqual(
methodNames,
['cash', 'cards', 'bitcoin'],
'renders in stable order: cash, cards, bitcoin'
);
});
test('it does not render payment methods when no payment tags are present on place', async function (assert) {
const place = {
title: 'Simple Place',
lat: 52.52,
lon: 13.4,
osmTags: {
amenity: 'bench',
},
};
await render(<template><PlaceDetails @place={{place}} /></template>);
assert.dom('.place-payment-methods').doesNotExist();
});
});
@@ -0,0 +1,177 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'marco/tests/helpers';
import { render } from '@ember/test-helpers';
import PlacePaymentMethods from 'marco/components/place-payment-methods';
module('Integration | Component | place-payment-methods', function (hooks) {
setupRenderingTest(hooks);
test('it renders nothing when no payment tags are present', async function (assert) {
await render(<template><PlacePaymentMethods @tags={{hash}} /></template>);
assert.dom('.place-payment-methods').doesNotExist();
const otherTags = { amenity: 'restaurant', name: 'Bistro' };
await render(
<template><PlacePaymentMethods @tags={{otherTags}} /></template>
);
assert.dom('.place-payment-methods').doesNotExist();
});
test('it renders cash and card icons when accepted with aria-description and no title attribute', async function (assert) {
const tags = {
'payment:cash': 'yes',
'payment:cards': 'yes',
};
await render(<template><PlacePaymentMethods @tags={{tags}} /></template>);
assert.dom('.place-payment-methods').exists();
assert.dom('[data-test-payment-method="cash"]').exists();
assert
.dom('[data-test-payment-method="cash"]')
.doesNotHaveClass('is-denied');
assert
.dom('[data-test-payment-method="cash"]')
.doesNotHaveAttribute('title');
assert
.dom('[data-test-payment-method="cash"]')
.hasAttribute('aria-description', 'Cash accepted');
assert
.dom('[data-test-payment-method="cash"] > .icon')
.hasAttribute('style', /width:22px;height:22px;color:currentColor/);
assert.dom('[data-test-payment-method="cards"]').exists();
assert
.dom('[data-test-payment-method="cards"]')
.doesNotHaveClass('is-denied');
assert
.dom('[data-test-payment-method="cards"]')
.doesNotHaveAttribute('title');
assert
.dom('[data-test-payment-method="cards"]')
.hasAttribute('aria-description', 'Cards accepted');
assert
.dom('[data-test-payment-method="cards"] > .icon')
.hasAttribute('style', /width:22px;height:22px;color:currentColor/);
assert.dom('[data-test-payment-method="bitcoin"]').doesNotExist();
});
test('it shows strike-through when cards are explicitly denied', async function (assert) {
const tags = {
'payment:cash': 'yes',
'payment:cards': 'no',
};
await render(<template><PlacePaymentMethods @tags={{tags}} /></template>);
assert.dom('[data-test-payment-method="cards"]').exists();
assert.dom('[data-test-payment-method="cards"]').hasClass('is-denied');
assert
.dom('[data-test-payment-method="cash"]')
.doesNotHaveClass('is-denied');
});
test('it shows strike-through when cash is explicitly denied', async function (assert) {
const tags = {
'payment:cash': 'no',
'payment:cards': 'yes',
};
await render(<template><PlacePaymentMethods @tags={{tags}} /></template>);
assert.dom('[data-test-payment-method="cash"]').exists();
assert.dom('[data-test-payment-method="cash"]').hasClass('is-denied');
assert
.dom('[data-test-payment-method="cards"]')
.doesNotHaveClass('is-denied');
});
test('it renders on-chain bitcoin without lightning zap badge', async function (assert) {
const tags = {
'currency:XBT': 'yes',
'payment:onchain': 'yes',
'payment:lightning': 'no',
};
await render(<template><PlacePaymentMethods @tags={{tags}} /></template>);
assert.dom('[data-test-payment-method="bitcoin"]').exists();
assert
.dom('[data-test-payment-method="bitcoin"]')
.doesNotHaveClass('is-denied');
assert.dom('[data-test-payment-badge="lightning"]').doesNotExist();
});
test('it renders bitcoin with lightning zap badge when lightning is accepted', async function (assert) {
const tags = {
'currency:XBT': 'yes',
'payment:lightning': 'yes',
};
await render(<template><PlacePaymentMethods @tags={{tags}} /></template>);
assert.dom('[data-test-payment-method="bitcoin"]').exists();
assert
.dom('[data-test-payment-method="bitcoin"]')
.doesNotHaveClass('is-denied');
assert
.dom('[data-test-payment-method="bitcoin"]')
.doesNotHaveAttribute('title');
assert
.dom('[data-test-payment-method="bitcoin"]')
.hasAttribute('aria-description', 'Bitcoin (Lightning) accepted');
assert
.dom('[data-test-payment-method="bitcoin"] > .icon')
.hasAttribute('style', /width:17px;height:22px;color:currentColor/);
assert.dom('[data-test-payment-badge="lightning"]').exists();
assert
.dom('[data-test-payment-badge="lightning"] .icon')
.hasClass('icon-filled');
assert
.dom('[data-test-payment-badge="lightning"] .icon')
.hasAttribute('style', /width:13px/);
assert
.dom('[data-test-payment-badge="lightning"] .icon')
.hasAttribute('style', /color:#fff/);
assert
.dom('[data-test-payment-badge="lightning"]')
.doesNotHaveAttribute('title');
});
test('it renders bitcoin with strike-through when explicitly denied', async function (assert) {
const tags = {
'currency:XBT': 'no',
};
await render(<template><PlacePaymentMethods @tags={{tags}} /></template>);
assert.dom('[data-test-payment-method="bitcoin"]').exists();
assert.dom('[data-test-payment-method="bitcoin"]').hasClass('is-denied');
assert
.dom('[data-test-payment-method="bitcoin"]')
.doesNotHaveAttribute('title');
assert
.dom('[data-test-payment-method="bitcoin"]')
.hasAttribute('aria-description', 'No Bitcoin');
});
test('it preserves stable order (cash, cards, bitcoin) regardless of whether denied or accepted', async function (assert) {
const tags = {
'payment:cash': 'no',
'payment:cards': 'yes',
'currency:XBT': 'yes',
'payment:lightning': 'yes',
};
await render(<template><PlacePaymentMethods @tags={{tags}} /></template>);
const methods = Array.from(
this.element.querySelectorAll('[data-test-payment-method]')
).map((el) => el.getAttribute('data-test-payment-method'));
assert.deepEqual(
methods,
['cash', 'cards', 'bitcoin'],
'icons stay in fixed positions: cash, cards, bitcoin'
);
});
});
+218
View File
@@ -0,0 +1,218 @@
import { parsePaymentMethods } from 'marco/utils/payment';
import { module, test } from 'qunit';
module('Unit | Utility | payment', function () {
test('it returns hasPaymentInfo: false for empty or non-payment tags', function (assert) {
assert.deepEqual(parsePaymentMethods(undefined), {
hasPaymentInfo: false,
cash: null,
cards: null,
bitcoin: { status: null, lightning: false, onchain: false },
});
assert.deepEqual(parsePaymentMethods({}), {
hasPaymentInfo: false,
cash: null,
cards: null,
bitcoin: { status: null, lightning: false, onchain: false },
});
assert.deepEqual(
parsePaymentMethods({ amenity: 'cafe', name: 'Coffee Shop' }),
{
hasPaymentInfo: false,
cash: null,
cards: null,
bitcoin: { status: null, lightning: false, onchain: false },
}
);
});
module('cash', function () {
test('it identifies cash as accepted', function (assert) {
assert.strictEqual(
parsePaymentMethods({ 'payment:cash': 'yes' }).cash,
'accepted'
);
assert.strictEqual(
parsePaymentMethods({ 'payment:cash': 'only' }).cash,
'accepted'
);
assert.strictEqual(
parsePaymentMethods({ 'payment:coins': 'yes' }).cash,
'accepted'
);
assert.strictEqual(
parsePaymentMethods({ 'payment:notes': 'yes' }).cash,
'accepted'
);
});
test('it identifies cash as denied', function (assert) {
assert.strictEqual(
parsePaymentMethods({ 'payment:cash': 'no' }).cash,
'denied'
);
assert.strictEqual(
parsePaymentMethods({ 'payment:coins': 'no', 'payment:notes': 'no' })
.cash,
'denied'
);
});
test('it keeps cash as null when cash tags are absent', function (assert) {
assert.strictEqual(
parsePaymentMethods({ 'payment:cards': 'yes' }).cash,
null
);
});
});
module('cards', function () {
test('it identifies cards as accepted from generic or card tags', function (assert) {
assert.strictEqual(
parsePaymentMethods({ 'payment:cards': 'yes' }).cards,
'accepted'
);
assert.strictEqual(
parsePaymentMethods({ 'payment:credit_cards': 'yes' }).cards,
'accepted'
);
assert.strictEqual(
parsePaymentMethods({ 'payment:debit_cards': 'yes' }).cards,
'accepted'
);
assert.strictEqual(
parsePaymentMethods({ 'payment:contactless': 'yes' }).cards,
'accepted'
);
assert.strictEqual(
parsePaymentMethods({ 'payment:visa': 'yes' }).cards,
'accepted'
);
assert.strictEqual(
parsePaymentMethods({ 'payment:mastercard': 'yes' }).cards,
'accepted'
);
assert.strictEqual(
parsePaymentMethods({ 'payment:american_express': 'yes' }).cards,
'accepted'
);
assert.strictEqual(
parsePaymentMethods({ 'payment:girocard': 'yes' }).cards,
'accepted'
);
assert.strictEqual(
parsePaymentMethods({ 'payment:apple_pay': 'yes' }).cards,
'accepted'
);
assert.strictEqual(
parsePaymentMethods({ 'payment:google_pay': 'yes' }).cards,
'accepted'
);
});
test('it identifies cards as denied when explicitly disabled', function (assert) {
assert.strictEqual(
parsePaymentMethods({ 'payment:cards': 'no' }).cards,
'denied'
);
assert.strictEqual(
parsePaymentMethods({
'payment:credit_cards': 'no',
'payment:debit_cards': 'no',
}).cards,
'denied'
);
assert.strictEqual(
parsePaymentMethods({ 'payment:cash': 'only' }).cards,
'denied'
);
});
test('it accepts cards if debit is accepted even if credit_cards is no', function (assert) {
assert.strictEqual(
parsePaymentMethods({
'payment:credit_cards': 'no',
'payment:debit_cards': 'yes',
}).cards,
'accepted'
);
});
});
module('bitcoin', function () {
test('it identifies on-chain bitcoin acceptance', function (assert) {
const result = parsePaymentMethods({ 'currency:XBT': 'yes' });
assert.strictEqual(result.bitcoin.status, 'accepted');
assert.true(result.bitcoin.onchain);
assert.false(result.bitcoin.lightning);
});
test('it identifies bitcoin from payment:onchain tag', function (assert) {
const result = parsePaymentMethods({ 'payment:onchain': 'yes' });
assert.strictEqual(result.bitcoin.status, 'accepted');
assert.true(result.bitcoin.onchain);
assert.false(result.bitcoin.lightning);
});
test('it identifies bitcoin with lightning', function (assert) {
const result = parsePaymentMethods({
'currency:XBT': 'yes',
'payment:lightning': 'yes',
'payment:onchain': 'no',
});
assert.strictEqual(result.bitcoin.status, 'accepted');
assert.true(result.bitcoin.lightning);
assert.false(result.bitcoin.onchain);
});
test('it identifies bitcoin with both lightning and on-chain', function (assert) {
const result = parsePaymentMethods({
'currency:XBT': 'yes',
'payment:lightning': 'yes',
'payment:onchain': 'yes',
});
assert.strictEqual(result.bitcoin.status, 'accepted');
assert.true(result.bitcoin.lightning);
assert.true(result.bitcoin.onchain);
});
test('it supports fallback bitcoin tags', function (assert) {
assert.strictEqual(
parsePaymentMethods({ 'payment:bitcoin': 'yes' }).bitcoin.status,
'accepted'
);
assert.strictEqual(
parsePaymentMethods({ 'currency:BTC': 'yes' }).bitcoin.status,
'accepted'
);
});
test('it identifies bitcoin as denied when explicitly no', function (assert) {
assert.strictEqual(
parsePaymentMethods({ 'currency:XBT': 'no' }).bitcoin.status,
'denied'
);
assert.strictEqual(
parsePaymentMethods({ 'payment:bitcoin': 'no' }).bitcoin.status,
'denied'
);
});
});
test('it handles mixed tags and sets hasPaymentInfo to true', function (assert) {
const result = parsePaymentMethods({
'payment:cash': 'yes',
'payment:cards': 'no',
'currency:XBT': 'yes',
'payment:lightning': 'yes',
});
assert.true(result.hasPaymentInfo);
assert.strictEqual(result.cash, 'accepted');
assert.strictEqual(result.cards, 'denied');
assert.strictEqual(result.bitcoin.status, 'accepted');
assert.true(result.bitcoin.lightning);
});
});