Fix review findings: expiry bug (all 8 files), idempotency keys, card_expiry/card_cvc removal, URL encoding, BuyerEmail logging, ValidateCardInfo, saved-card test, future work doc
Backend: - Fix refund idempotency key: clock.Now() → deterministic (pr.ID + amount) - Fix ValidateCardInfo: enforce mutual exclusivity, handle empty strings symmetrically - Fix paymentFromSquare brand fallback (remove dead SourceType fallback) - Fix URL encoding: PathEscape → QueryEscape for customer_id query param - Fix BuyerEmail: log warning on DB error instead of silent discard - Fix idempotency key in createCardOnFileHTTP: time.Now() → deterministic hex hash - Add BuyerEmail to CreateTipPayment Square request - Move realBaseURL from shared file to square_dev.go (only used in dev) - Add TestTipPayment_WithSavedCard test (card_id path coverage) - Fix AMEX brand in mock (AMEX → AMERICAN_EXPRESS, fix test) Frontend: - Fix off-by-month expiry bug in ALL 8 files using year-month arithmetic (parseExpiryParts returns 1-indexed, SvelteDate expects 0-indexed) Files: tip/+page, pay-tip/[id], UserBookingModal, UserPaymentModal, BookingFlow, account/+page (add card + buy gift card sections) - Remove card_expiry/card_cvc from tip request bodies (backend has no fields) Docs: - Mark P9 (placeholder tokens) as completed, add P11 (Square Web Payments SDK) - Mark T13 (rune arithmetic) as completed
This commit is contained in:
@@ -199,8 +199,10 @@ import CardBrandIcon from '$lib/components/payments/CardBrandIcon.svelte';
|
||||
const isTipNewCardExpiryPast = $derived(
|
||||
tipNewCardExpiryParts !== null &&
|
||||
(() => {
|
||||
const expiryDate = new SvelteDate(tipNewCardExpiryParts.year, tipNewCardExpiryParts.month);
|
||||
return expiryDate < new SvelteDate();
|
||||
const expiryYearMonth = tipNewCardExpiryParts.year * 12 + tipNewCardExpiryParts.month;
|
||||
const now = new SvelteDate();
|
||||
const currentYearMonth = now.getFullYear() * 12 + now.getMonth() + 1;
|
||||
return expiryYearMonth < currentYearMonth;
|
||||
})()
|
||||
);
|
||||
const hasTipNewCardInvalidMonth = $derived(/^\d{2}\/\d{2}$/.test(tipNewCardExpiry) && tipNewCardExpiryParts === null);
|
||||
@@ -322,8 +324,6 @@ import CardBrandIcon from '$lib/components/payments/CardBrandIcon.svelte';
|
||||
|
||||
if (tipShowNewCard || tipSavedCards.length === 0) {
|
||||
body.new_card_token = tipNewCardNumber.replace(/\s/g, '');
|
||||
body.card_expiry = tipNewCardExpiry;
|
||||
body.card_cvc = tipNewCardCVC;
|
||||
body.save_card = tipSaveCardFuture;
|
||||
} else {
|
||||
body.card_id = tipSelectedCardId;
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
const cardError = $derived(
|
||||
cardNumberTouched && !isValidLuhn(newCardNumber) && newCardNumber.length > 0
|
||||
? 'Invalid card number'
|
||||
: cardExpiryTouched && expiryParts !== null && (() => { const d = new SvelteDate(expiryParts.year, expiryParts.month); return d < new SvelteDate(); })()
|
||||
: cardExpiryTouched && expiryParts !== null && (() => { const em = expiryParts.year * 12 + expiryParts.month; const now2 = new SvelteDate(); const cm = now2.getFullYear() * 12 + now2.getMonth() + 1; return em < cm; })()
|
||||
? 'This card has expired'
|
||||
: cardExpiryTouched && !/^\d{2}\/\d{2}$/.test(newCardExpiry) && newCardExpiry.length > 0
|
||||
? 'Enter expiry as MM/YY'
|
||||
|
||||
@@ -79,8 +79,10 @@
|
||||
const isExpiryInPast = $derived(
|
||||
expiryParts !== null &&
|
||||
(() => {
|
||||
const expiryDate = new SvelteDate(expiryParts.year, expiryParts.month);
|
||||
return expiryDate < new SvelteDate();
|
||||
const expiryYearMonth = expiryParts.year * 12 + expiryParts.month;
|
||||
const now = new SvelteDate();
|
||||
const currentYearMonth = now.getFullYear() * 12 + now.getMonth() + 1;
|
||||
return expiryYearMonth < currentYearMonth;
|
||||
})()
|
||||
);
|
||||
|
||||
|
||||
@@ -201,8 +201,10 @@ import CardBrandIcon from '$lib/components/payments/CardBrandIcon.svelte';
|
||||
const isNewCardExpiryInPast = $derived(
|
||||
newCardExpiryParts !== null &&
|
||||
(() => {
|
||||
const expiryDate = new SvelteDate(newCardExpiryParts.year, newCardExpiryParts.month);
|
||||
return expiryDate < new SvelteDate();
|
||||
const expiryYearMonth = newCardExpiryParts.year * 12 + newCardExpiryParts.month;
|
||||
const now = new SvelteDate();
|
||||
const currentYearMonth = now.getFullYear() * 12 + now.getMonth() + 1;
|
||||
return expiryYearMonth < currentYearMonth;
|
||||
})()
|
||||
);
|
||||
const isNewCardExpiryInvalidMonth = $derived(
|
||||
@@ -230,8 +232,10 @@ import CardBrandIcon from '$lib/components/payments/CardBrandIcon.svelte';
|
||||
const isBuyNewCardExpiryInPast = $derived(
|
||||
buyNewCardExpiryParts !== null &&
|
||||
(() => {
|
||||
const expiryDate = new SvelteDate(buyNewCardExpiryParts.year, buyNewCardExpiryParts.month);
|
||||
return expiryDate < new SvelteDate();
|
||||
const expiryYearMonth = buyNewCardExpiryParts.year * 12 + buyNewCardExpiryParts.month;
|
||||
const now = new SvelteDate();
|
||||
const currentYearMonth = now.getFullYear() * 12 + now.getMonth() + 1;
|
||||
return expiryYearMonth < currentYearMonth;
|
||||
})()
|
||||
);
|
||||
const isBuyNewCardExpiryInvalidMonth = $derived(
|
||||
|
||||
@@ -112,8 +112,10 @@ import CardBrandIcon from '$lib/components/payments/CardBrandIcon.svelte';
|
||||
const isNewCardExpiryPast = $derived(
|
||||
newCardExpiryParts !== null &&
|
||||
(() => {
|
||||
const expiryDate = new SvelteDate(newCardExpiryParts.year, newCardExpiryParts.month);
|
||||
return expiryDate < new SvelteDate();
|
||||
const expiryYearMonth = newCardExpiryParts.year * 12 + newCardExpiryParts.month;
|
||||
const now = new SvelteDate();
|
||||
const currentYearMonth = now.getFullYear() * 12 + now.getMonth() + 1;
|
||||
return expiryYearMonth < currentYearMonth;
|
||||
})()
|
||||
);
|
||||
const hasNewCardInvalidMonth = $derived(/^\d{2}\/\d{2}$/.test(newCardExpiry) && newCardExpiryParts === null);
|
||||
@@ -306,8 +308,6 @@ import CardBrandIcon from '$lib/components/payments/CardBrandIcon.svelte';
|
||||
|
||||
if (showNewCardForm || savedCards.length === 0) {
|
||||
body.new_card_token = newCardNumber.replace(/\s/g, '');
|
||||
body.card_expiry = newCardExpiry;
|
||||
body.card_cvc = newCardCVC;
|
||||
body.save_card = saveCardForFuture;
|
||||
} else {
|
||||
body.card_id = selectedCardId;
|
||||
|
||||
@@ -136,8 +136,12 @@ import CardBrandIcon from '$lib/components/payments/CardBrandIcon.svelte';
|
||||
const isNewCardExpiryPast = $derived(
|
||||
newCardExpiryParts !== null &&
|
||||
(() => {
|
||||
const expiryDate = new SvelteDate(newCardExpiryParts.year, newCardExpiryParts.month);
|
||||
return expiryDate < new SvelteDate();
|
||||
// parseExpiryParts returns months 1-indexed (1=Jan, 12=Dec).
|
||||
// Use year-month arithmetic to avoid Date constructor 0-index confusion.
|
||||
const expiryYearMonth = newCardExpiryParts.year * 12 + newCardExpiryParts.month;
|
||||
const now = new SvelteDate();
|
||||
const currentYearMonth = now.getFullYear() * 12 + now.getMonth() + 1;
|
||||
return expiryYearMonth < currentYearMonth;
|
||||
})()
|
||||
);
|
||||
const hasNewCardInvalidMonth = $derived(/^\d{2}\/\d{2}$/.test(newCardExpiry) && newCardExpiryParts === null);
|
||||
@@ -253,8 +257,6 @@ import CardBrandIcon from '$lib/components/payments/CardBrandIcon.svelte';
|
||||
|
||||
if (showNewCardForm || savedCards.length === 0) {
|
||||
body.new_card_token = newCardNumber.replace(/\s/g, '');
|
||||
body.card_expiry = newCardExpiry;
|
||||
body.card_cvc = newCardCVC;
|
||||
body.save_card = saveCardForFuture;
|
||||
} else {
|
||||
body.card_id = selectedCardId;
|
||||
|
||||
Reference in New Issue
Block a user