From 628f94d3e2621102fcf48fcaf0660265ff4c5825 Mon Sep 17 00:00:00 2001
From: Abban Dunne <abban.dunne@wikimedia.de>
Date: Thu, 19 Dec 2024 11:14:53 +0100
Subject: [PATCH] Implement VAR for C24_WMDE_Desktop_DE_23

VAR has a donation receipt checkbox that pre-fills
the option on the main donation form.

Ticket: https://phabricator.wikimedia.org/T382307
---
 .../C24_WMDE_Desktop_DE_23/banner_var.ts      |   2 +-
 .../components/BannerVar.vue                  |  13 +-
 ...inDonationFormPaymentsAndReceiptButton.vue |  37 +++
 .../MainDonationFormReceiptAboveValue.vue     | 172 ++++++++++++++
 .../C24_WMDE_Desktop_DE_23/messages.ts        |   3 +-
 .../styles/styles_var.scss                    |  60 +++++
 .../C24_WMDE_Desktop_DE_23/useFormAction.ts   |  22 ++
 .../SubComponents/SubmitValues.vue            |   3 +-
 src/components/composables/useFormModel.ts    |   6 +-
 src/themes/Treedip/swatches/color_light.scss  |   7 +-
 src/utils/FormModel/FormModel.ts              |   1 +
 .../components/BannerVar.spec.ts              |  46 +++-
 ...nationFormPaymentsAndReceiptButton.spec.ts | 109 +++++++++
 .../MainDonationFormReceiptAboveValue.spec.ts | 217 ++++++++++++++++++
 test/resetFormModel.ts                        |   1 +
 15 files changed, 689 insertions(+), 10 deletions(-)
 create mode 100644 banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormPaymentsAndReceiptButton.vue
 create mode 100644 banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormReceiptAboveValue.vue
 create mode 100644 banners/desktop/C24_WMDE_Desktop_DE_23/styles/styles_var.scss
 create mode 100644 banners/desktop/C24_WMDE_Desktop_DE_23/useFormAction.ts
 create mode 100644 test/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormPaymentsAndReceiptButton.spec.ts
 create mode 100644 test/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormReceiptAboveValue.spec.ts

diff --git a/banners/desktop/C24_WMDE_Desktop_DE_23/banner_var.ts b/banners/desktop/C24_WMDE_Desktop_DE_23/banner_var.ts
index c49bfc19d..dd8c20eaa 100644
--- a/banners/desktop/C24_WMDE_Desktop_DE_23/banner_var.ts
+++ b/banners/desktop/C24_WMDE_Desktop_DE_23/banner_var.ts
@@ -1,6 +1,6 @@
 import { createVueApp } from '@src/createVueApp';
 
-import './styles/styles.scss';
+import './styles/styles_var.scss';
 
 import BannerConductor from '@src/components/BannerConductor/FallbackBannerConductor.vue';
 import Banner from './components/BannerVar.vue';
diff --git a/banners/desktop/C24_WMDE_Desktop_DE_23/components/BannerVar.vue b/banners/desktop/C24_WMDE_Desktop_DE_23/components/BannerVar.vue
index e1cae3382..d149c6562 100644
--- a/banners/desktop/C24_WMDE_Desktop_DE_23/components/BannerVar.vue
+++ b/banners/desktop/C24_WMDE_Desktop_DE_23/components/BannerVar.vue
@@ -34,12 +34,13 @@
 			<template #donation-form="{ formInteraction }: any">
 				<MultiStepDonation
 					:step-controllers="stepControllers"
-					@form-interaction="formInteraction"
 					:submit-callback="onSubmit"
+					:form-action-override="formAction"
+					@form-interaction="formInteraction"
 				>
 
 					<template #[FormStepNames.MainDonationFormStep]="{ pageIndex, submit, isCurrent, previous }: any">
-						<MainDonationForm :page-index="pageIndex" @submit="submit" :is-current="isCurrent" @previous="previous"/>
+						<MainDonationForm :page-index="pageIndex" @submit="submit" :is-current="isCurrent" :show-receipt-checkbox-below="minimumAmount" @previous="previous"/>
 					</template>
 
 					<template #[FormStepNames.UpgradeToYearlyFormStep]="{ pageIndex, submit, isCurrent, previous }: any">
@@ -88,7 +89,7 @@ import FundsModal from '@src/components/UseOfFunds/FundsModal.vue';
 import BannerText from '../content/BannerText.vue';
 import BannerSlides from '../content/BannerSlides.vue';
 import MultiStepDonation from '@src/components/DonationForm/MultiStepDonation.vue';
-import MainDonationForm from '@src/components/DonationForm/Forms/MainDonationForm.vue';
+import MainDonationForm from './MainDonationFormReceiptAboveValue.vue';
 import UpgradeToYearlyButtonForm from '@src/components/DonationForm/Forms/UpgradeToYearlyButtonForm.vue';
 import KeenSlider from '@src/components/Slider/KeenSlider.vue';
 import { useFormModel } from '@src/components/composables/useFormModel';
@@ -111,6 +112,10 @@ import { BannerSubmitOnReturnEvent } from '@src/tracking/events/BannerSubmitOnRe
 import { Tracker } from '@src/tracking/Tracker';
 import { useBannerHider } from '@src/components/composables/useBannerHider';
 import BannerTitle from '@banners/desktop/C24_WMDE_Desktop_DE_15/content/BannerTitle.vue';
+import { useFormAction } from '../useFormAction';
+import { FormActions } from '@src/domain/FormActions';
+
+const minimumAmount = 10;
 
 enum ContentStates {
 	Main = 'wmde-banner-wrapper--main',
@@ -143,6 +148,8 @@ const stepControllers = [
 	createSubmittableUpgradeToYearly( formModel, FormStepNames.MainDonationFormStep, FormStepNames.MainDonationFormStep )
 ];
 
+const { formAction } = useFormAction( inject<FormActions>( 'formActions' ), minimumAmount );
+
 watch( contentState, async () => {
 	emit( 'bannerContentChanged' );
 } );
diff --git a/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormPaymentsAndReceiptButton.vue b/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormPaymentsAndReceiptButton.vue
new file mode 100644
index 000000000..aa8637bc8
--- /dev/null
+++ b/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormPaymentsAndReceiptButton.vue
@@ -0,0 +1,37 @@
+<template>
+	<button class="wmde-banner-form-button t-submit-main-donation" type="submit">
+		{{ submitButtonLabel }}
+	</button>
+</template>
+
+<script setup lang="ts">
+import { computed, inject } from 'vue';
+import { PaymentMethods } from '@src/utils/FormItemsBuilder/fields/PaymentMethods';
+import { useFormModel } from '@src/components/composables/useFormModel';
+import { Translator } from '@src/Translator';
+import { AddressTypes } from '@src/utils/FormItemsBuilder/fields/AddressTypes';
+
+interface Props {
+	paymentLabelsBelow: number;
+}
+const props = defineProps<Props>();
+
+const formModel = useFormModel();
+const translator = inject<Translator>( 'translator' );
+const { paymentMethod, addressType, receipt, numericAmount } = formModel;
+
+const submitButtonLabel = computed( (): string => {
+	if ( numericAmount.value < props.paymentLabelsBelow && !receipt.value && [ AddressTypes.ANONYMOUS.value, '' ].includes( addressType.value ) ) {
+		if ( paymentMethod.value === PaymentMethods.PAYPAL.value ) {
+			return translator.translate( 'submit-label-paypal' );
+		} else if ( paymentMethod.value === PaymentMethods.CREDIT_CARD.value ) {
+			return translator.translate( 'submit-label-credit-card' );
+		} else if ( paymentMethod.value === PaymentMethods.SOFORT.value ) {
+			return translator.translate( 'submit-label-sofort' );
+		} else if ( paymentMethod.value === PaymentMethods.BANK_TRANSFER.value ) {
+			return translator.translate( 'submit-label-bank-transfer' );
+		}
+	}
+	return translator.translate( 'submit-label' );
+} );
+</script>
diff --git a/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormReceiptAboveValue.vue b/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormReceiptAboveValue.vue
new file mode 100644
index 000000000..3a12f7580
--- /dev/null
+++ b/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormReceiptAboveValue.vue
@@ -0,0 +1,172 @@
+<template>
+	<form
+		method="post"
+		class="wmde-banner-sub-form wmde-banner-sub-form-donation"
+		@submit.prevent="validate"
+	>
+		<fieldset class="wmde-banner-form-field-group">
+			<legend class="wmde-banner-form-field-group-legend">{{ $translate( 'intervals-header' ) }}</legend>
+			<SelectGroup
+				:field-name="'select-interval'"
+				:selectionItems="formItems.intervals"
+				:isValid="isValidOrUnset( intervalValidity )"
+				:errorMessage="$translate( 'no-interval-message' )"
+				v-model:inputValue="interval"
+				:disabledOptions="disabledIntervals"
+			/>
+		</fieldset>
+
+		<fieldset class="wmde-banner-form-field-group">
+			<legend class="wmde-banner-form-field-group-legend">{{ $translate( 'amounts-header' ) }}</legend>
+			<SelectGroup
+				fieldName="select-amount"
+				:selectionItems="formItems.amounts"
+				:isValid="isValidOrUnset( amountValidity )"
+				:errorMessage="$translate( amountValidityMessageKey( amountValidity ) )"
+				v-model:inputValue="selectedAmount"
+			>
+				<SelectCustomAmount
+					fieldName="select-amount"
+					v-model:inputValue="customAmount"
+					@focus="clearSelectedAmount"
+					@blur="formatCustomAmount"
+					:placeholder="$translate( customAmountPlaceholderKey )"
+				/>
+			</SelectGroup>
+		</fieldset>
+
+		<fieldset class="wmde-banner-form-field-group">
+			<legend class="wmde-banner-form-field-group-legend">{{ $translate( 'payments-header' ) }}</legend>
+			<SelectGroup
+				:field-name="'select-payment-method'"
+				:selectionItems="formItems.paymentMethods"
+				:isValid="isValidOrUnset( paymentMethodValidity )"
+				:errorMessage="$translate( 'no-payment-type-message' )"
+				v-model:inputValue="paymentMethod"
+				:disabledOptions="disabledPaymentMethods"
+			>
+				<template #select-group-label="{ label, slotName }: any">
+					<slot :name="'label-' + slotName" :label="label"/>
+				</template>
+				<SmsBox>
+					<template #sms-icon>
+						<slot name="sms-icon"/>
+					</template>
+				</SmsBox>
+			</SelectGroup>
+		</fieldset>
+
+		<div class="wmde-banner-form-donation-receipt-checkbox" v-if="showReceiptCheckbox">
+			<input
+				class="wmde-banner-form-field-checkbox"
+				type="checkbox"
+				value="person"
+				id="wmde-banner-form-donation-receipt"
+				v-model="receipt"
+			>
+			<label for="wmde-banner-form-donation-receipt">
+				{{ $translate( 'donation-receipt-checkbox-label' ) }}
+			</label>
+		</div>
+
+		<div class="wmde-banner-form-button-container">
+			<slot name="button">
+				<MainDonationFormButtonMultiStep :payment-labels-below="showReceiptCheckboxBelow"/>
+			</slot>
+			<button v-if="!isFormValid && showErrorScrollLink" class="wmde-banner-form-button-error">
+				{{ $translate( 'global-error' ) }}
+			</button>
+		</div>
+	</form>
+</template>
+
+<script lang="ts">
+// All form components must have names
+export default {
+	name: 'MainDonationFormDonationReceipt'
+};
+</script>
+<script setup lang="ts">
+
+import { computed, inject, onMounted, ref, watch } from 'vue';
+import SelectGroup from '@src/components/DonationForm/SubComponents/SelectGroup.vue';
+import { DonationFormItems } from '@src/utils/FormItemsBuilder/DonationFormItems';
+import SelectCustomAmount from '@src/components/DonationForm/SubComponents/SelectCustomAmount.vue';
+import SmsBox from '@src/components/DonationForm/SubComponents/SmsBox.vue';
+import { useFormModel } from '@src/components/composables/useFormModel';
+import { newDonationFormValidator } from '@src/validation/DonationFormValidator';
+import { amountValidityMessageKey } from '@src/utils/amountValidityMessageKey';
+import { isValidOrUnset } from '@src/components/DonationForm/Forms/isValidOrUnset';
+import { Currency } from '@src/utils/DynamicContent/formatters/Currency';
+import MainDonationFormButtonMultiStep from './MainDonationFormPaymentsAndReceiptButton.vue';
+import { AddressTypes } from '@src/utils/FormItemsBuilder/fields/AddressTypes';
+import { PaymentMethods } from '@src/utils/FormItemsBuilder/fields/PaymentMethods';
+
+interface Props {
+	showReceiptCheckboxBelow: number;
+	showErrorScrollLink?: boolean;
+	customAmountPlaceholderKey?: string;
+}
+
+const props = withDefaults( defineProps<Props>(), {
+	showErrorScrollLink: false,
+	customAmountPlaceholderKey: 'custom-amount-placeholder'
+} );
+const emit = defineEmits( [ 'submit' ] );
+
+const currencyFormatter = inject<Currency>( 'currencyFormatter' );
+const formItems = inject<DonationFormItems>( 'formItems' );
+const formModel = useFormModel();
+const validator = newDonationFormValidator( formModel );
+const showReceiptCheckbox = computed<boolean>( () => {
+	if ( interval.value === '' ) {
+		return false;
+	}
+	if ( paymentMethod.value === '' || paymentMethod.value === PaymentMethods.DIRECT_DEBIT.value ) {
+		return false;
+	}
+
+	if ( numericAmount.value >= props.showReceiptCheckboxBelow ) {
+		return false;
+	}
+
+	return true;
+} );
+const isFormValid = ref<boolean>( true );
+
+const validate = (): void => {
+	isFormValid.value = validator.validate();
+
+	if ( isFormValid.value ) {
+		emit( 'submit' );
+	}
+};
+
+const {
+	interval, intervalValidity, disabledIntervals,
+	selectedAmount, customAmount, numericAmount, amountValidity,
+	paymentMethod, paymentMethodValidity, disabledPaymentMethods,
+	addressType, receipt
+} = formModel;
+
+const clearSelectedAmount = (): void => {
+	selectedAmount.value = '';
+};
+
+const formatCustomAmount = (): void => {
+	if ( customAmount.value !== '' ) {
+		customAmount.value = currencyFormatter.customAmountInput( numericAmount.value );
+	}
+};
+
+watch( [ interval, paymentMethod, numericAmount ], () => {
+	if ( !showReceiptCheckbox.value ) {
+		receipt.value = false;
+	}
+} );
+
+onMounted( () => {
+	addressType.value = AddressTypes.ANONYMOUS.value;
+} );
+
+</script>
diff --git a/banners/desktop/C24_WMDE_Desktop_DE_23/messages.ts b/banners/desktop/C24_WMDE_Desktop_DE_23/messages.ts
index 6af854a63..a708091b0 100644
--- a/banners/desktop/C24_WMDE_Desktop_DE_23/messages.ts
+++ b/banners/desktop/C24_WMDE_Desktop_DE_23/messages.ts
@@ -29,7 +29,8 @@ const messages: TranslationMessages = {
 	'upgrade-to-yearly-yes': 'Ja, ich spende {{amount}} jährlich',
 	'campaign-day-only-n-days': 'Heute sind es nur noch {{days}} Tage bis zum Ende unserer Spendenkampagne.',
 	'custom-amount-placeholder': 'Wahlbetrag',
-	'upgrade-to-yearly-header': 'Bitte spenden Sie {{amount}} jährlich!'
+	'upgrade-to-yearly-header': 'Bitte spenden Sie {{amount}} jährlich!',
+	'donation-receipt-checkbox-label': 'Bitte senden Sie mir eine steuerlich absetzbare Spendenbescheinigung an meine Postanschrift.'
 };
 
 export default messages;
diff --git a/banners/desktop/C24_WMDE_Desktop_DE_23/styles/styles_var.scss b/banners/desktop/C24_WMDE_Desktop_DE_23/styles/styles_var.scss
new file mode 100644
index 000000000..9064eff80
--- /dev/null
+++ b/banners/desktop/C24_WMDE_Desktop_DE_23/styles/styles_var.scss
@@ -0,0 +1,60 @@
+// This is the file where we import the theme-specific component styles
+@use 'src/themes/Treedip/swatches/skin_default' with (
+	$slider: true,
+	$select-group: true,
+	$upgrade-to-yearly: true,
+	$fallback-banner: true,
+	$double-progress-bar: true,
+	$soft-close: true,
+);
+@use 'src/components/BannerConductor/banner-transition';
+@use 'src/themes/UseOfFunds/swatches/skin_default' as uof-default;
+@use 'Banner';
+@use 'src/themes/UseOfFunds/UseOfFunds';
+@use 'MainBanner' with (
+	$banner-height: 357px,
+	$form-width: 300px
+);
+@use 'src/themes/Treedip/defaults';
+@use 'src/themes/Treedip/ButtonClose/ButtonClose';
+@use 'src/themes/Treedip/ProgressBar/DoubleProgressBar';
+@use 'src/themes/Treedip/DonationForm/DonationForm';
+@use 'src/themes/Treedip/DonationForm/MultiStepDonation';
+@use 'src/themes/Treedip/DonationForm/SubComponents/SelectGroup';
+@use 'src/themes/Treedip/DonationForm/SubComponents/SelectGroupRadios';
+@use 'src/themes/Treedip/DonationForm/SubComponents/SelectCustomAmountRadio';
+@use 'src/themes/Treedip/DonationForm/SubComponents/SmsBox';
+@use 'src/themes/Treedip/DonationForm/Forms/MainDonationFormDonationReceipt' with (
+	$padding: 14px 0 0
+);
+@use 'src/themes/Treedip/DonationForm/Forms/UpgradeToYearlyButtonForm' with (
+	$font-size: 14px
+);
+@use 'src/themes/Treedip/DonationForm/Forms/CustomAmountForm';
+@use 'src/themes/Treedip/Footer/FooterAlreadyDonated';
+@use 'src/themes/Treedip/Footer/SelectionInput';
+@use 'src/themes/Treedip/Message/Message'  with (
+	$slider-main-headline-font-size: 25px,
+	$message-header-padding-bottom: 8px,
+	$message-header-small-up-padding-bottom: 8px
+);
+@use 'src/themes/Treedip/SoftClose/SoftClose';
+@use 'src/themes/Treedip/Slider/KeenSlider' with (
+	$slider-padding: 0
+);
+
+/**
+ * Fallback banner with "Fijitiv" theme
+ * All selectors in Fijitiv theme are prefixed with the ".wmde-banner-fallback" class selector,
+  so they override the "default" styles with the same selector
+ */
+@use 'FallbackBanner';
+@use 'src/themes/Fijitiv/FallbackBanner/FallbackButton';
+@use 'src/themes/Fijitiv/FallbackBanner/LargeFooter' with (
+	$fallback-large-footer-right-before-border-color: white
+);
+@use 'src/themes/Fijitiv/FallbackBanner/SmallFooter';
+@use 'src/themes/Fijitiv/ProgressBar/ProgressBar' as FallbackProgressBar with (
+	$progress-bar-margin: 0 15px
+);
+@use 'src/themes/Fijitiv/Slider/KeenSlider' as FallbackSlider;
diff --git a/banners/desktop/C24_WMDE_Desktop_DE_23/useFormAction.ts b/banners/desktop/C24_WMDE_Desktop_DE_23/useFormAction.ts
new file mode 100644
index 000000000..079f08a64
--- /dev/null
+++ b/banners/desktop/C24_WMDE_Desktop_DE_23/useFormAction.ts
@@ -0,0 +1,22 @@
+import { FormActions } from '@src/domain/FormActions';
+import { computed, Ref } from 'vue';
+import { useFormModel } from '@src/components/composables/useFormModel';
+import { PaymentMethods } from '@src/utils/FormItemsBuilder/fields/PaymentMethods';
+
+export function useFormAction( formActions: FormActions, minimumAmount: number ): { formAction: Ref<string> } {
+	const formModel = useFormModel();
+	const formAction = computed( (): string => {
+
+		let URL: string = formActions.donateAnonymouslyAction;
+
+		if ( formModel.numericAmount.value >= minimumAmount || formModel.receipt.value || formModel.paymentMethod.value === PaymentMethods.DIRECT_DEBIT.value ) {
+			URL = formActions.donateWithAddressAction + '&ap=1';
+		}
+
+		return URL;
+	} );
+
+	return {
+		formAction
+	};
+}
diff --git a/src/components/DonationForm/SubComponents/SubmitValues.vue b/src/components/DonationForm/SubComponents/SubmitValues.vue
index 6de3e2c71..320ccad5f 100644
--- a/src/components/DonationForm/SubComponents/SubmitValues.vue
+++ b/src/components/DonationForm/SubComponents/SubmitValues.vue
@@ -4,6 +4,7 @@
 		<input type="hidden" name="amount" :value="formattedAmountForServer" />
 		<input type="hidden" name="interval" :value="interval" />
 		<input type="hidden" name="paymentType" :value="paymentMethod" />
+		<input type="hidden" name="receipt" :value="receipt" />
 	</div>
 </template>
 
@@ -12,7 +13,7 @@
 import { useFormModel } from '@src/components/composables/useFormModel';
 import { computed } from 'vue';
 
-const { addressType, totalNumericAmount, interval, paymentMethod } = useFormModel();
+const { addressType, totalNumericAmount, interval, paymentMethod, receipt } = useFormModel();
 
 const formattedAmountForServer = computed( (): number => {
 	return parseFloat( totalNumericAmount.value.toFixed( 2 ) ) * 100;
diff --git a/src/components/composables/useFormModel.ts b/src/components/composables/useFormModel.ts
index ed6719070..26ce3254b 100644
--- a/src/components/composables/useFormModel.ts
+++ b/src/components/composables/useFormModel.ts
@@ -32,6 +32,8 @@ const paymentMethodValidity = ref<Validity>( Validity.Unset );
 const addressType = ref<string>( '' );
 const addressTypeValidity = ref<Validity>( Validity.Unset );
 
+const receipt = ref<boolean|null>( null );
+
 const hasTransactionFee = ref<boolean>( false );
 
 const disabledIntervals = computed( (): string[] => {
@@ -121,8 +123,10 @@ export function useFormModel(): FormModel {
 		paymentMethodValidity,
 		disabledPaymentMethods,
 		addressType,
-
 		addressTypeValidity,
+
+		receipt,
+
 		disabledAddressTypes,
 
 		hasTransactionFee,
diff --git a/src/themes/Treedip/swatches/color_light.scss b/src/themes/Treedip/swatches/color_light.scss
index 869f4712e..174738ab1 100644
--- a/src/themes/Treedip/swatches/color_light.scss
+++ b/src/themes/Treedip/swatches/color_light.scss
@@ -58,8 +58,8 @@ $blue700: #2a4b8d;
 
 	--input-checkbox-background: transparent;
 	--input-checkbox-border: #{$grey500};
-	--input-checkbox-background-checked: #{$blue700};
-	--input-checkbox-border-checked: #{$blue700};
+	--input-checkbox-background-checked: #{$red600};
+	--input-checkbox-border-checked: #{$red600};
 
 	--animated-highlight-color: #{$grey700};
 	/* stylelint-disable */
@@ -131,6 +131,9 @@ $blue700: #2a4b8d;
 		--select-group-bubble-radio-background-checked: #{$white};
 		--select-group-bubble-radio-border-checked: #{$blue700};
 		--select-group-bubble-radio-checkmark: #{$blue700};
+
+		--input-checkbox-background-checked: #{$blue700};
+		--input-checkbox-border-checked: #{$blue700};
 	}
 
 	@if $select-group-button {
diff --git a/src/utils/FormModel/FormModel.ts b/src/utils/FormModel/FormModel.ts
index 193c03ed3..2e583afc4 100644
--- a/src/utils/FormModel/FormModel.ts
+++ b/src/utils/FormModel/FormModel.ts
@@ -30,6 +30,7 @@ export interface FormModel {
 	disabledPaymentMethods: ComputedRef<string[]>;
 	addressType: Ref<string>;
 	addressTypeValidity: Ref<Validity>;
+	receipt: Ref<boolean|null>;
 	disabledAddressTypes: Ref<string[]>;
 	/**
 	 * Flag to indicate that a payment-provider-specific transaction fee should be added to the total amount
diff --git a/test/banners/desktop/C24_WMDE_Desktop_DE_23/components/BannerVar.spec.ts b/test/banners/desktop/C24_WMDE_Desktop_DE_23/components/BannerVar.spec.ts
index faab3dbc0..e4f0f6239 100644
--- a/test/banners/desktop/C24_WMDE_Desktop_DE_23/components/BannerVar.spec.ts
+++ b/test/banners/desktop/C24_WMDE_Desktop_DE_23/components/BannerVar.spec.ts
@@ -1,4 +1,4 @@
-import { beforeEach, describe, test, vi } from 'vitest';
+import { beforeEach, describe, expect, it, test, vi } from 'vitest';
 import { mount, VueWrapper } from '@vue/test-utils';
 import Banner from '@banners/desktop/C24_WMDE_Desktop_DE_23/components/BannerVar.vue';
 import { BannerStates } from '@src/components/BannerConductor/StateMachine/BannerStates';
@@ -134,6 +134,50 @@ describe( 'BannerVar.vue', () => {
 		] )( '%s', async ( testName: string ) => {
 			await formActionSwitchFeatures[ testName ]( getWrapper() );
 		} );
+
+		it( 'Set the correct action when amount is above 10', async (): Promise<void> => {
+			const wrapper = getWrapper();
+
+			await wrapper.find( '.interval-0 input' ).trigger( 'change' );
+			await wrapper.find( '.amount-10 input' ).trigger( 'change' );
+			await wrapper.find( '.payment-ppl input' ).trigger( 'change' );
+
+			expect( wrapper.find<HTMLFormElement>( '.wmde-banner-submit-form' ).element.action ).toContain( 'with-address&ap=1' );
+		} );
+
+		it( 'Set the correct action when amount is below 10 and receipt is not checked', async (): Promise<void> => {
+			const wrapper = getWrapper();
+
+			await wrapper.find( '.interval-0 input' ).trigger( 'change' );
+			await wrapper.find( '.amount-5 input' ).trigger( 'change' );
+			await wrapper.find( '.payment-ppl input' ).trigger( 'change' );
+
+			expect( wrapper.find<HTMLFormElement>( '.wmde-banner-submit-form' ).element.action ).toContain( 'without-address' );
+		} );
+
+		it( 'Set the correct action when amount is below 10 and receipt is checked', async (): Promise<void> => {
+			const wrapper = getWrapper();
+
+			await wrapper.find( '.interval-0 input' ).trigger( 'change' );
+			await wrapper.find( '.amount-5 input' ).trigger( 'change' );
+			await wrapper.find( '.payment-ppl input' ).trigger( 'change' );
+			await wrapper.find( '#wmde-banner-form-donation-receipt' ).trigger( 'click' );
+
+			expect( wrapper.find<HTMLFormElement>( '.wmde-banner-submit-form' ).element.action ).toContain( 'with-address&ap=1' );
+		} );
+
+		it( 'Puts the receipt option into the submit form', async (): Promise<void> => {
+			const wrapper = getWrapper();
+
+			expect( wrapper.find<HTMLInputElement>( '.wmde-banner-submit-form [name="receipt"]' ).element.value ).toStrictEqual( 'false' );
+
+			await wrapper.find( '.interval-0 input' ).trigger( 'change' );
+			await wrapper.find( '.amount-5 input' ).trigger( 'change' );
+			await wrapper.find( '.payment-ppl input' ).trigger( 'change' );
+			await wrapper.find( '#wmde-banner-form-donation-receipt' ).trigger( 'click' );
+
+			expect( wrapper.find<HTMLInputElement>( '.wmde-banner-submit-form [name="receipt"]' ).element.value ).toStrictEqual( 'true' );
+		} );
 	} );
 
 	describe( 'Soft Close', () => {
diff --git a/test/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormPaymentsAndReceiptButton.spec.ts b/test/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormPaymentsAndReceiptButton.spec.ts
new file mode 100644
index 000000000..d1228f5eb
--- /dev/null
+++ b/test/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormPaymentsAndReceiptButton.spec.ts
@@ -0,0 +1,109 @@
+import { beforeEach, describe, expect, it, test } from 'vitest';
+import { useFormModel } from '@src/components/composables/useFormModel';
+import { shallowMount } from '@vue/test-utils';
+import { PaymentMethods } from '@src/utils/FormItemsBuilder/fields/PaymentMethods';
+import MainDonationFormPaymentsAndReceiptButton from '@banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormPaymentsAndReceiptButton.vue';
+import { AddressTypes } from '@src/utils/FormItemsBuilder/fields/AddressTypes';
+import { resetFormModel } from '@test/resetFormModel';
+
+const translate = ( key: string ): string => key;
+const formModel = useFormModel();
+
+describe( 'MainDonationFormPaymentsAndReceiptButton.vue', () => {
+
+	beforeEach( () => resetFormModel( formModel ) );
+
+	test.each( [
+		[ AddressTypes.ANONYMOUS.value, 'submit-label-paypal' ],
+		[ '', 'submit-label-paypal' ],
+		[ AddressTypes.EMAIL.value, 'submit-label' ],
+		[ AddressTypes.FULL.value, 'submit-label' ]
+	] )( 'shows the correct label for address type %s', ( addressType: string, label: string ) => {
+		formModel.addressType.value = addressType;
+		formModel.paymentMethod.value = PaymentMethods.PAYPAL.value;
+		formModel.customAmount.value = '9.99';
+
+		const wrapper = shallowMount( MainDonationFormPaymentsAndReceiptButton, {
+			props: {
+				paymentLabelsBelow: 10
+			},
+			global: {
+				provide: {
+					translator: { translate }
+				}
+			}
+		} );
+
+		expect( wrapper.text() ).toStrictEqual( label );
+	} );
+
+	test.each( [
+		[ true, 'submit-label' ],
+		[ false, 'submit-label-paypal' ],
+		[ null, 'submit-label-paypal' ]
+	] )( 'shows the correct label for receipt %s', ( receipt: boolean|null, label: string ) => {
+		formModel.receipt.value = receipt;
+		formModel.paymentMethod.value = PaymentMethods.PAYPAL.value;
+		formModel.addressType.value = AddressTypes.ANONYMOUS.value;
+		formModel.customAmount.value = '9.99';
+
+		const wrapper = shallowMount( MainDonationFormPaymentsAndReceiptButton, {
+			props: {
+				paymentLabelsBelow: 10
+			},
+			global: {
+				provide: {
+					translator: { translate }
+				}
+			}
+		} );
+
+		expect( wrapper.text() ).toStrictEqual( label );
+	} );
+
+	test.each( [
+		[ PaymentMethods.PAYPAL.value, 'submit-label-paypal' ],
+		[ PaymentMethods.BANK_TRANSFER.value, 'submit-label-bank-transfer' ],
+		[ PaymentMethods.CREDIT_CARD.value, 'submit-label-credit-card' ],
+		[ PaymentMethods.SOFORT.value, 'submit-label-sofort' ],
+		[ PaymentMethods.DIRECT_DEBIT.value, 'submit-label' ]
+	] )( 'shows the correct label when anonymous for payment method %s', ( paymentMethod: string, label: string ) => {
+		formModel.addressType.value = AddressTypes.ANONYMOUS.value;
+		formModel.paymentMethod.value = paymentMethod;
+		formModel.customAmount.value = '9.99';
+
+		const wrapper = shallowMount( MainDonationFormPaymentsAndReceiptButton, {
+			props: {
+				paymentLabelsBelow: 10
+			},
+			global: {
+				provide: {
+					translator: { translate }
+				}
+			}
+		} );
+
+		expect( wrapper.text() ).toStrictEqual( label );
+	} );
+
+	it( 'shows the correct label when amount is 10 or higher', () => {
+		formModel.receipt.value = false;
+		formModel.paymentMethod.value = PaymentMethods.PAYPAL.value;
+		formModel.addressType.value = AddressTypes.ANONYMOUS.value;
+		formModel.customAmount.value = '10';
+
+		const wrapper = shallowMount( MainDonationFormPaymentsAndReceiptButton, {
+			props: {
+				paymentLabelsBelow: 10
+			},
+			global: {
+				provide: {
+					translator: { translate }
+				}
+			}
+		} );
+
+		expect( wrapper.text() ).toStrictEqual( 'submit-label' );
+	} );
+
+} );
diff --git a/test/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormReceiptAboveValue.spec.ts b/test/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormReceiptAboveValue.spec.ts
new file mode 100644
index 000000000..fdf8d6dfe
--- /dev/null
+++ b/test/banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormReceiptAboveValue.spec.ts
@@ -0,0 +1,217 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { mount, VueWrapper } from '@vue/test-utils';
+import DonationForm from '@banners/desktop/C24_WMDE_Desktop_DE_23/components/MainDonationFormReceiptAboveValue.vue';
+import { DonationFormItems } from '@src/utils/FormItemsBuilder/DonationFormItems';
+import { Intervals } from '@src/utils/FormItemsBuilder/fields/Intervals';
+import { PaymentMethods } from '@src/utils/FormItemsBuilder/fields/PaymentMethods';
+import { newDonationFormValidator } from '@src/validation/DonationFormValidator';
+import { useFormModel } from '@src/components/composables/useFormModel';
+import { resetFormModel } from '@test/resetFormModel';
+import { CurrencyEn } from '@src/utils/DynamicContent/formatters/CurrencyEn';
+import { TrackerSpy } from '@test/fixtures/TrackerSpy';
+import { AddressTypes } from '@src/utils/FormItemsBuilder/fields/AddressTypes';
+
+const formItems: DonationFormItems = {
+	addressType: [],
+	amounts: [
+		{ value: '1', label: '€1', className: 'amount-1' },
+		{ value: '5', label: '€5', className: 'amount-5' }
+	],
+	intervals: [ Intervals.ONCE, Intervals.MONTHLY ],
+	paymentMethods: [ PaymentMethods.PAYPAL, PaymentMethods.CREDIT_CARD, PaymentMethods.DIRECT_DEBIT ]
+};
+
+vi.mock( '@src/validation/DonationFormValidator', () => {
+	return {
+		newDonationFormValidator: vi.fn()
+	};
+} );
+
+const formModel = useFormModel();
+const translate = ( key: string ): string => key;
+
+describe( 'MainDonationFormReceiptAboveValue.vue', () => {
+
+	// The model values are in the global scope, and they need to be reset before each test
+	beforeEach( () => resetFormModel( formModel ) );
+
+	const getWrapper = ( showErrorScrollLink: boolean = false ): VueWrapper<any> => {
+		return mount( DonationForm, {
+			props: {
+				showErrorScrollLink,
+				showReceiptCheckboxBelow: 10
+			},
+			global: {
+				mocks: {
+					$translate: translate
+				},
+				provide: {
+					currencyFormatter: new CurrencyEn(),
+					formActions: { donateWithAddressAction: 'https://example.com', donateWithoutAddressAction: 'https://example.com' },
+					formItems: formItems,
+					translator: { translate },
+					tracker: new TrackerSpy()
+				}
+			},
+			attachTo: document.body
+		} );
+	};
+
+	it( 'should clear selected amount when input field is focused', () => {
+		const input = getWrapper().find<HTMLInputElement>( '.wmde-banner-select-custom-amount-input' );
+		formModel.selectedAmount.value = '100';
+
+		input.trigger( 'focus' );
+
+		expect( formModel.selectedAmount.value ).toBe( '' );
+	} );
+
+	it( 'should format custom amount when custom amount input field is blurred and it has a value', async () => {
+		const input = getWrapper().find<HTMLInputElement>( '.wmde-banner-select-custom-amount-input' );
+
+		await input.setValue( '3,14' );
+		await input.trigger( 'blur' );
+
+		expect( input.element.value ).toBe( '3.14' );
+	} );
+
+	it( 'should not format custom amount to 0.00 when input field is blurred and is blank', async () => {
+		const input = getWrapper().find<HTMLInputElement>( '.wmde-banner-select-custom-amount-input' );
+
+		await input.trigger( 'blur' );
+
+		expect( input.element.value ).toBe( '' );
+	} );
+
+	it( 'shows invalid fields on submit when fields are invalid', async () => {
+		vi.mocked( newDonationFormValidator ).mockReturnValue( { validate: () => false } );
+		const wrapper = getWrapper();
+
+		await wrapper.trigger( 'submit' );
+
+		expect( wrapper.find( '.select-interval .wmde-banner-select-group-error-message' ).exists() ).toBeTruthy();
+		expect( wrapper.find( '.select-amount .wmde-banner-select-group-error-message' ).exists() ).toBeTruthy();
+		expect( wrapper.find( '.select-payment-method .wmde-banner-select-group-error-message' ).exists() ).toBeTruthy();
+	} );
+
+	it( 'shows the error scroll link when form fields are invalid', async () => {
+		vi.mocked( newDonationFormValidator ).mockReturnValue( { validate: () => false } );
+		const wrapper = getWrapper( true );
+
+		await wrapper.trigger( 'submit' );
+
+		expect( wrapper.find( '.wmde-banner-form-button-error' ).exists() ).toBeTruthy();
+	} );
+
+	it( 'emits an event on submit when fields are valid', () => {
+		vi.mocked( newDonationFormValidator ).mockReturnValue( { validate: () => true } );
+		const wrapper = getWrapper();
+
+		wrapper.trigger( 'submit' );
+
+		expect( wrapper.emitted( 'submit' ).length ).toBe( 1 );
+	} );
+
+	it( 'does not emit our own submit event when form fields are invalid', () => {
+		vi.mocked( newDonationFormValidator ).mockReturnValue( { validate: () => false } );
+		const wrapper = getWrapper();
+
+		wrapper.trigger( 'submit' );
+
+		expect( wrapper.emitted( 'submit' ) ).toBeUndefined();
+	} );
+
+	it( 'passes payment label slots dynamically to select group', () => {
+		const wrapper = mount( DonationForm, {
+			props: {
+				showErrorScrollLink: false,
+				showReceiptCheckboxBelow: 10
+			},
+			slots: {
+				'label-payment-ppl': `<template #label-payment-ppl><span class="custom-label-paypal"></span></template>`,
+				'label-payment-mcp': `<template #label-payment-mcp><span class="custom-label-credit-cards"></span></template>`
+			},
+			global: {
+				mocks: {
+					$translate: translate
+				},
+				provide: {
+					currencyFormatter: new CurrencyEn(),
+					formActions: { donateWithAddressAction: 'https://example.com', donateWithoutAddressAction: 'https://example.com' },
+					formItems: formItems,
+					translator: { translate },
+					tracker: new TrackerSpy()
+				}
+			}
+		} );
+
+		expect( wrapper.find( '.custom-label-paypal' ).exists() ).toBeTruthy();
+		expect( wrapper.find( '.custom-label-credit-cards' ).exists() ).toBeTruthy();
+	} );
+
+	it( 'sets the address type to anonymous when mounted', async () => {
+		expect( formModel.addressType.value ).toStrictEqual( '' );
+		getWrapper();
+		expect( formModel.addressType.value ).toStrictEqual( AddressTypes.ANONYMOUS.value );
+	} );
+
+	it( 'shows the donation receipt checkbox', async () => {
+		const wrapper = getWrapper();
+
+		expect( wrapper.find( '.wmde-banner-form-donation-receipt-checkbox' ).exists() ).toBeFalsy();
+
+		await wrapper.find( '.interval-0 .wmde-banner-select-group-input' ).trigger( 'change' );
+		await wrapper.find( '.amount-5 .wmde-banner-select-group-input' ).trigger( 'change' );
+		await wrapper.find( '.payment-ppl .wmde-banner-select-group-input' ).trigger( 'change' );
+
+		expect( wrapper.find( '.wmde-banner-form-donation-receipt-checkbox' ).exists() ).toBeTruthy();
+	} );
+
+	it( 'does not show the donation receipt checkbox when address type is direct debit', async () => {
+		const wrapper = getWrapper();
+
+		expect( wrapper.find( '.wmde-banner-form-donation-receipt-checkbox' ).exists() ).toBeFalsy();
+
+		await wrapper.find( '.interval-0 .wmde-banner-select-group-input' ).trigger( 'change' );
+		await wrapper.find( '.amount-5 .wmde-banner-select-group-input' ).trigger( 'change' );
+		await wrapper.find( '.payment-bez .wmde-banner-select-group-input' ).trigger( 'change' );
+
+		expect( wrapper.find( '.wmde-banner-form-donation-receipt-checkbox' ).exists() ).toBeFalsy();
+	} );
+
+	it( 'updates the form model receipt', async () => {
+		const wrapper = getWrapper();
+
+		await wrapper.find( '.interval-0 .wmde-banner-select-group-input' ).trigger( 'change' );
+		await wrapper.find( '.amount-5 .wmde-banner-select-group-input' ).trigger( 'change' );
+		await wrapper.find( '.payment-ppl .wmde-banner-select-group-input' ).trigger( 'change' );
+
+		expect( formModel.receipt.value ).toStrictEqual( false );
+
+		await wrapper.find( '#wmde-banner-form-donation-receipt' ).trigger( 'click' );
+
+		expect( formModel.receipt.value ).toStrictEqual( true );
+
+		await wrapper.find( '#wmde-banner-form-donation-receipt' ).trigger( 'click' );
+
+		expect( formModel.receipt.value ).toStrictEqual( false );
+	} );
+
+	it( 'clears the receipt option when radio field becomes hidden', async () => {
+		const wrapper = getWrapper();
+
+		await wrapper.find( '.interval-0 .wmde-banner-select-group-input' ).trigger( 'change' );
+		await wrapper.find( '.amount-5 .wmde-banner-select-group-input' ).trigger( 'change' );
+		await wrapper.find( '.payment-ppl .wmde-banner-select-group-input' ).trigger( 'change' );
+
+		expect( formModel.receipt.value ).toStrictEqual( false );
+
+		await wrapper.find( '#wmde-banner-form-donation-receipt' ).trigger( 'click' );
+
+		expect( formModel.receipt.value ).toStrictEqual( true );
+
+		await wrapper.find( '.payment-bez .wmde-banner-select-group-input' ).trigger( 'change' );
+
+		expect( formModel.receipt.value ).toStrictEqual( false );
+	} );
+} );
diff --git a/test/resetFormModel.ts b/test/resetFormModel.ts
index 9eddcda68..87e1e0108 100644
--- a/test/resetFormModel.ts
+++ b/test/resetFormModel.ts
@@ -12,5 +12,6 @@ export function resetFormModel( formModel: FormModel ): void {
 	formModel.paymentMethodValidity.value = Validity.Unset;
 	formModel.addressType.value = '';
 	formModel.addressTypeValidity.value = Validity.Unset;
+	formModel.receipt.value = null;
 	formModel.hasTransactionFee.value = false;
 }