diff --git a/.github/workflows/build-on-push.yml b/.github/workflows/build-on-push.yml index c984ed3863..b400ca3a68 100644 --- a/.github/workflows/build-on-push.yml +++ b/.github/workflows/build-on-push.yml @@ -7,10 +7,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - postgresql: ['10','16'] + postgresql: ['10', '15','16'] steps: - - uses: actions/checkout@v3 - - uses: actions/cache@v3 + - uses: actions/checkout@v4 + - uses: actions/cache@v4 with: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} @@ -23,7 +23,7 @@ jobs: restore-keys: | ${{ runner.os }}-gradlew- - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: temurin @@ -37,7 +37,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: 'Upload Build' if: ${{ github.repository == 'alfio-event/alf.io' && matrix.postgresql == '10'}} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: dist path: build @@ -49,7 +49,7 @@ jobs: name: Push dev image steps: - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: dist - name: Set up QEMU @@ -79,7 +79,7 @@ jobs: name: Push PROD image steps: - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: dist - name: Set up QEMU diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8badc47ab6..2133974a20 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. @@ -27,7 +27,7 @@ jobs: # languages: go, javascript, csharp, python, cpp, java - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: temurin diff --git a/.gitignore b/.gitignore index 0fda5edfaa..0abdbe4e7f 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,6 @@ logs/ .DS_Store out/ .clever.json -!/src/main/webapp/resources/bower_components/angular-growl-v2/build/ alfio-itest .gradletasknamecache public/ diff --git a/build.gradle b/build.gradle index 96fcc67662..b818815129 100644 --- a/build.gradle +++ b/build.gradle @@ -38,7 +38,7 @@ plugins { id 'com.github.ben-manes.versions' version '0.51.0' id 'com.github.hierynomus.license' version '0.16.1' id 'net.researchgate.release' version '3.0.2' - id 'org.springframework.boot' version '3.3.0-RC1' + id 'org.springframework.boot' version '3.3.0' id 'io.spring.dependency-management' version '1.1.4' id 'org.sonarqube' version '5.0.0.4638' // id 'net.ltgt.errorprone' version '3.1.0' diff --git a/frontend/.editorconfig b/frontend/.editorconfig index 59d9a3a3e7..0792692308 100644 --- a/frontend/.editorconfig +++ b/frontend/.editorconfig @@ -4,7 +4,7 @@ root = true [*] charset = utf-8 indent_style = space -indent_size = 2 +indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true diff --git a/frontend/admin/package-lock.json b/frontend/admin/package-lock.json index 8f2c045d45..f433e45108 100644 --- a/frontend/admin/package-lock.json +++ b/frontend/admin/package-lock.json @@ -11,6 +11,7 @@ "@lit/context": "1.1.1", "@lit/task": "1.0.0", "@shoelace-style/shoelace": "2.15.0", + "@tanstack/lit-form": "^0.20.3", "lit": "3.1.3" }, "devDependencies": { @@ -733,6 +734,42 @@ "url": "https://github.com/sponsors/claviska" } }, + "node_modules/@tanstack/form-core": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-0.20.3.tgz", + "integrity": "sha512-oFu1fBSQyDL7fkvXxP2ZPnXWFcac973xghNPfiF8DMUzJ7MBFe4sZHB5HS6l3YGWtMFnqRwW4sFIhHLGKtDVJA==", + "dependencies": { + "@tanstack/store": "^0.3.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/lit-form": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@tanstack/lit-form/-/lit-form-0.20.3.tgz", + "integrity": "sha512-WFJL4j0CYiC43n248rS7Zm9QTgPqs1s6sMkmJJ/B//TPsKYW6UhWAfbVFFMBCtG9yPTw7avdXhiXGTv6nVoPIQ==", + "dependencies": { + "@tanstack/form-core": "0.20.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "lit": "^3.1.1" + } + }, + "node_modules/@tanstack/store": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.3.1.tgz", + "integrity": "sha512-A49KN8SpLMWaNmZGPa9K982RQ81W+m7W6iStcQVeKeVS70JZRqkF0fDwKByREPq6qz9/kS0aQFOPQ0W6wIeU5g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", diff --git a/frontend/admin/package.json b/frontend/admin/package.json index daf532fa40..7988e072d6 100644 --- a/frontend/admin/package.json +++ b/frontend/admin/package.json @@ -12,6 +12,7 @@ "@lit/context": "1.1.1", "@lit/task": "1.0.0", "@shoelace-style/shoelace": "2.15.0", + "@tanstack/lit-form": "^0.20.3", "lit": "3.1.3" }, "devDependencies": { diff --git a/frontend/admin/src/display-common-mark-preview/display-common-mark-preview.ts b/frontend/admin/src/display-common-mark-preview/display-common-mark-preview.ts new file mode 100644 index 0000000000..6ef318f882 --- /dev/null +++ b/frontend/admin/src/display-common-mark-preview/display-common-mark-preview.ts @@ -0,0 +1,73 @@ +import {customElement, property, state} from "lit/decorators.js"; +import {unsafeHTML} from 'lit/directives/unsafe-html.js'; +import {css, html, LitElement, TemplateResult} from "lit"; +import {Task} from "@lit/task"; +import {UtilService} from "../service/util.ts"; +import {row} from "../styles.ts"; + +@customElement('alfio-display-commonmark-preview') +export class DisplayCommonMarkPreview extends LitElement { + + static styles = [ + row, + css` + sl-button.left::part(base) { + justify-content: start; + } + sl-button.right::part(base) { + justify-content: end; + } + ` + ] + + @property({ type: String, attribute: 'data-button-text' }) + buttonText?: string; + + @property({ type: String, attribute: 'data-text' }) + text?: string; + + @state() + dialogOpen = false; + + private task?: Task, string>; + + protected render(): TemplateResult { + return html` +
+ How to format text + ${this.buttonText} +
+ +
+ ${this.renderText()} +
+ Close +
+ `; + } + + private openDialog(): void { + this.dialogOpen = true; + this.task = new Task, string>(this, + async () => { + return await UtilService.renderMarkdown(this.text!); + }, () => []); + } + + private closeDialog(): void { + this.dialogOpen = false; + } + + private renderText(): TemplateResult { + return this.task?.render({ + initial: () => html`init`, + complete: (value) => html`${unsafeHTML(value)}` + }) ?? html`error`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'alfio-display-commonmark-preview': DisplayCommonMarkPreview + } +} diff --git a/frontend/admin/src/event/additional-item-edit/additional-item-edit.ts b/frontend/admin/src/event/additional-item-edit/additional-item-edit.ts new file mode 100644 index 0000000000..66187fd2f9 --- /dev/null +++ b/frontend/admin/src/event/additional-item-edit/additional-item-edit.ts @@ -0,0 +1,469 @@ +import {html, LitElement, TemplateResult} from "lit"; +import {customElement, query} from "lit/decorators.js"; +import { + AdditionalItem, + AdditionalItemTaxType, + AdditionalItemType, + isMandatoryPercentage, + SupplementPolicy, + supplementPolicyDescriptions, + taxTypeDescriptions +} from "../../model/additional-item.ts"; +import {SlDialog, SlRequestCloseEvent} from "@shoelace-style/shoelace"; +import {AlfioEvent, ContentLanguage} from "../../model/event.ts"; +import {repeat} from "lit/directives/repeat.js"; +import {TanStackFormController} from "@tanstack/lit-form"; +import {FormState} from '@tanstack/form-core'; +import {dialog, form, pageHeader, row, textColors} from "../../styles.ts"; +import {extractDateTime, notifyChange, toDateTimeModification} from "../../service/helpers.ts"; +import {classMap} from "lit/directives/class-map.js"; +import {AdditionalItemService} from "../../service/additional-item.ts"; + +@customElement('alfio-additional-item-edit') +export class AdditionalItemEdit extends LitElement { + + static styles = [pageHeader, row, dialog, form, textColors]; + + private editedItem: AdditionalItem | null = null; + private supportedLanguages: ContentLanguage[] = []; + private event: AlfioEvent | null = null; + private type: AdditionalItemType | null = null; + + @query("sl-dialog#editDialog") + dialog?: SlDialog; + + displayForm: boolean = false; + + // waiting for https://github.com/TanStack/form/pull/656 to be complete + private validationErrors?: { [k: string]: string}; + + #form: TanStackFormController = new TanStackFormController(this, { + defaultValues: { + descriptions: [] as DescriptionForm[], + availabilityAndPrices: {} as AvailabilityAndPricesForm, + } + }); + + private buildDefaultValues(currentState: FormState): FormData { + if (this.editedItem != null) { + const item = this.editedItem; + return { + availabilityAndPrices: this.buildAvailabilityAndPricesFromItem(item), + descriptions: item.title.map(((title, i) => { + const description = item.description[i]; + return { + locale: title.locale, + title: title.value, + description: description.value, + titleId: title.id, + descriptionId: description.id + } + })) + } + } + + + return { + availabilityAndPrices: this.event != null ? this.buildAvailabilityAndPricesFromEvent(this.event) : currentState.values.availabilityAndPrices, + descriptions: (this.supportedLanguages ?? []).map(sl => { + return { + locale: sl.locale, + title: '', + description: '', + titleId: null, + descriptionId: null + }; + }) + }; + } + + private buildAvailabilityAndPricesFromEvent(event: AlfioEvent): AvailabilityAndPricesForm { + const now = new Date(); + return { + availableItems: 0, + minPrice: null, + maxPrice: null, + price: 0, + fixPrice: this.type === 'SUPPLEMENT', + vat: event.vatPercentage, + vatType: "INHERITED", + supplementPolicy: "OPTIONAL_UNLIMITED_AMOUNT", + inception: `${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}T${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`, + expiration: event.begin, + maxQtyPerOrder: null, + availableQuantity: null + } + } + + private buildAvailabilityAndPricesFromItem(item: AdditionalItem): AvailabilityAndPricesForm { + return { + availableItems: item.availableItems !== -1 ? item.availableItems : null, + minPrice: item.minPrice, + maxPrice: item.maxPrice, + price: item.price, + fixPrice: item.fixPrice, + vat: item.vat, + vatType: item.vatType, + supplementPolicy: item.supplementPolicy, + inception: `${item.inception.date}T${item.inception.time}`, + expiration: `${item.expiration.date}T${item.expiration.time}`, + maxQtyPerOrder: item.maxQtyPerOrder ?? null, + availableQuantity: item.availableQuantity !== -1 ? item.availableQuantity ?? null : null + } + } + + protected render(): TemplateResult { + return html` + + + ${this.renderForm()} + + + `; + } + + private renderForm(): TemplateResult { + return html` +
{ e.preventDefault(); e.stopImmediatePropagation(); await this.#form.api.handleSubmit();}}> +

Descriptions

+
+ ${this.#form.field({name: 'descriptions'}, (descriptionsField) => this.renderDescription(descriptionsField.state.value))} +
+

Availability and prices

+
+ ${this.#form.field({name: 'availabilityAndPrices'}, (field) => this.renderAvailabilityAndPrices(field.state.value))} +
+
+ +
+ Close +
+ Save +
+
+
+ `; + } + + private renderDescription(descriptions: DescriptionForm[]): TemplateResult { + return html`${repeat(descriptions, (_, index) => index, (description, index) => { + return html` +
+
+

${this.supportedLanguages?.find(cl => cl.locale === description.locale)?.displayLanguage}

+
+ + ${ + // title + this.#form.field({name: `descriptions[${index}].title`, validators: { + onChange: ({ value }: { value: string }) => { + return value && value.length < 3 + ? 'error.title' + : undefined + }, + }}, + (field) => { + return html` + notifyChange(e, field)}> + ` + })} + ${this.#form.field({name: `descriptions[${index}].description`, validators: { + onChange: ({ value }: { value: string }) => { + return value && value.length < 3 + ? 'error.description' + : undefined + }, + }}, + (field) => { + return html` + notifyChange(e, field)}> +
+ +
+
+ ` + })} +
+ `; + })}`; + } + + private hasError(meta: any, name: string) { + return (meta.isTouched && meta.errors.length > 0) || this.validationError(name) != null; + } + + private renderAvailabilityAndPrices(formValue: AvailabilityAndPricesForm): TemplateResult { + return html` +
+ ${this.#form.field({name: `availabilityAndPrices.supplementPolicy`}, + (field) => { + return html` + notifyChange(e, field)}> + ${repeat(Object.keys(supplementPolicyDescriptions), k => k, (k) => html` + ${supplementPolicyDescriptions[k]} + `)} + ` + }) + } +
+
+
+ ${this.#form.field({name: `availabilityAndPrices.inception`, validators: { + onChange: ({ value }: { value: string }) => { + return value && value.length < 3 + ? 'Not long enough' + : undefined + }, + }}, + (field) => { + return html` + notifyChange(e, field)}> +
Please check the validity interval
+
` + }) + } +
+
+ ${this.#form.field({name: `availabilityAndPrices.expiration`, validators: { + onChange: ({ value }: { value: string }) => { + return value && value.length < 3 + ? 'Not long enough' + : undefined + }, + }}, + (field) => { + return html` + notifyChange(e, field)}> +
Please check the validity interval
+
` + }) + } +
+
+
+
+ ${this.#form.field({name: `availabilityAndPrices.price`}, + (field) => { + return html` + notifyChange(e, field)}> +
${isMandatoryPercentage(formValue.supplementPolicy) ? '%' : this.event?.currency}
+
` + }) + } +
+
+ ${this.#form.field({name: `availabilityAndPrices.vatType`}, + (field) => { + return html` + notifyChange(e, field)}> + ${repeat(Object.keys(taxTypeDescriptions), k => k, (k) => html` + ${taxTypeDescriptions[k]} + `)} + ` + }) + } +
+
+ ${this.handleContextBasedFields(formValue)} + `; + } + + private handleContextBasedFields(formValue: AvailabilityAndPricesForm): TemplateResult { + switch (formValue.supplementPolicy) { + case "MANDATORY_ONE_FOR_TICKET": + return html``; + case "MANDATORY_PERCENTAGE_RESERVATION": + case "MANDATORY_PERCENTAGE_FOR_TICKET": + return html` +
+
+ ${this.#form.field({name: `availabilityAndPrices.minPrice`}, + (field) => { + return html` + notifyChange(e, field)}> +
${this.event?.currency}
+
+ ` + }) + } +
+
+ ${this.#form.field({name: `availabilityAndPrices.maxPrice`}, + (field) => { + return html` + notifyChange(e, field)}> +
${this.event?.currency}
+
+ ` + }) + } +
+
+ `; + case "OPTIONAL_MAX_AMOUNT_PER_RESERVATION": + case "OPTIONAL_MAX_AMOUNT_PER_TICKET": + return html` +
+
+ ${this.#form.field({name: `availabilityAndPrices.maxQtyPerOrder`}, + (field) => { + return html` + notifyChange(e, field)}> + ` + }) + } +
+ ${this.addAvailableQuantity()} +
+ `; + default: + return html` + ${this.addAvailableQuantity()} + + `; + } + } + + private addAvailableQuantity() { + if (this.event?.supportsAdditionalItemsQuantity) { + return html` +
+ ${this.#form.field({name: `availabilityAndPrices.availableQuantity`}, + (field) => { + return html` + notifyChange(e, field)}> + ` + }) + } +
+ ` + } + return html``; + } + + public async open(request: { + editedItem: AdditionalItem | null, + supportedLanguages: ContentLanguage[], + event: AlfioEvent, + type: AdditionalItemType + }): Promise { + if (this.dialog != null) { + this.editedItem = request.editedItem; + this.supportedLanguages = request.supportedLanguages; + this.event = request.event; + this.type = request.type; + this.#form.api.update({ + defaultValues: this.buildDefaultValues(this.#form.api.state), + onSubmit: async (values) => { + await this.save(values.value); + }, + validators: { + onSubmitAsync: async props => { + const errors: { [k:string]: string} = {}; + const additionalItemRequest: Partial = this.buildServerPayload(props.value); + const result = await AdditionalItemService.validateAdditionalItem(additionalItemRequest); + if (!result.success) { + result.validationErrors.forEach(error => { + errors[error.fieldName] = error.code; + }); + this.validationErrors = errors; + return 'form contains errors'; + } else { + this.validationErrors = {}; + return undefined; + } + } + } + }); + this.displayForm = true; + await this.dialog?.show(); + } + return this.dialog != null; + } + + public async close(success: boolean = false): Promise { + if (this.dialog != null) { + await this.dialog.hide(); + } + this.dispatchEvent(new CustomEvent('alfio-dialog-closed', { detail: { success } })); + return this.dialog != null; + } + + private preventAccidentalClose(e: SlRequestCloseEvent): void { + if (e.detail.source === 'overlay') { + e.preventDefault(); + } else { + this.dispatchEvent(new CustomEvent('alfio-dialog-closed', { detail: { success: false } })); + } + } + + private buildServerPayload(value: FormData): Partial { + return { + id: this.editedItem?.id, + maxQtyPerOrder: value.availabilityAndPrices.maxQtyPerOrder ?? -1, + price: value.availabilityAndPrices.price, + availableQuantity: value.availabilityAndPrices.availableQuantity ?? -1, + inception: toDateTimeModification(value.availabilityAndPrices.inception), + expiration: toDateTimeModification(value.availabilityAndPrices.expiration), + title: value.descriptions.map(df => { return { locale: df.locale, type: 'TITLE', value: df.title, id: df.titleId }}), + description: value.descriptions.map(df => { return { locale: df.locale, type: 'DESCRIPTION', value: df.description, id: df.descriptionId }}), + supplementPolicy: value.availabilityAndPrices.supplementPolicy, + vatType: value.availabilityAndPrices.vatType, + vat: value.availabilityAndPrices.vat, + type: this.type ?? undefined, + fixPrice: value.availabilityAndPrices.fixPrice + }; + } + + private async save(value: FormData) { + const additionalItemRequest: Partial = this.buildServerPayload(value); + const update = await AdditionalItemService.updateAdditionalItem(additionalItemRequest, this.event!.id); + if (update.ok) { + await this.close(true); + } + } + + private validationError(fieldName: string): string | undefined { + const validationErrors = this.validationErrors; + if (validationErrors != null) { + const remoteFieldName = fieldName.substring(fieldName.lastIndexOf('.') + 1, fieldName.length); + return validationErrors[remoteFieldName]; + } + return undefined; + } +} + +interface DescriptionForm { + locale: string; + title: string; + titleId: number | null; + description: string; + descriptionId: number | null; +} + +interface AvailabilityAndPricesForm { + price: number; + fixPrice: boolean; + availableQuantity: number | null; + maxQtyPerOrder: number | null; + inception: string; + expiration: string; + vat: number | null; + vatType: AdditionalItemTaxType; + supplementPolicy: SupplementPolicy; + availableItems: number | null; + minPrice: number | null; + maxPrice: number | null; +} + +interface FormData { + descriptions: DescriptionForm[]; + availabilityAndPrices: AvailabilityAndPricesForm +} + +declare global { + interface HTMLElementTagNameMap { + 'alfio-additional-item-edit': AdditionalItemEdit + } +} diff --git a/frontend/admin/src/event/additional-item-list/additional-item-list.ts b/frontend/admin/src/event/additional-item-list/additional-item-list.ts new file mode 100644 index 0000000000..449cca1d5f --- /dev/null +++ b/frontend/admin/src/event/additional-item-list/additional-item-list.ts @@ -0,0 +1,362 @@ +import {css, html, LitElement, nothing, TemplateResult} from 'lit'; +import {customElement, property, query, state} from 'lit/decorators.js' +import {repeat} from 'lit/directives/repeat.js'; +import {AdditionalItemService, UsageCount} from "../../service/additional-item.ts"; +import {Task} from "@lit/task"; +import {AlfioEvent, ContentLanguage} from "../../model/event.ts"; +import { + AdditionalItem, + AdditionalItemType, isMandatory, + isMandatoryPercentage, + supplementPolicyDescriptions +} from "../../model/additional-item.ts"; +import {EventService} from "../../service/event.ts"; +import {renderIf, supportedLanguages} from "../../service/helpers.ts"; +import {pageHeader, textColors} from "../../styles.ts"; +import {when} from "lit/directives/when.js"; +import {AdditionalItemEdit} from "../additional-item-edit/additional-item-edit.ts"; +import {AlfioDialogClosed, dispatchFeedback} from "../../model/dom-events.ts"; +import {ConfirmationDialogService} from "../../service/confirmation-dialog.ts"; + +interface Model { + event: AlfioEvent; + title: string; + icon: string; + type: AdditionalItemType; + supportedLanguages: ContentLanguage[]; + dataTask: Task, ListData>; +} + +interface ListData { + items: Array; + usageCount: UsageCount; + allowDownload: boolean; +} + +@customElement('alfio-additional-item-list') +export class AdditionalItemList extends LitElement { + + @property({ type: String, attribute: 'data-public-identifier' }) + publicIdentifier?: string; + @property({ type: String, attribute: 'data-type' }) + type?: AdditionalItemType; + @property({ type: String, attribute: 'data-title' }) + pageTitle?: string; + @property({ type: String, attribute: 'data-icon' }) + icon?: string; + @state() + editActive: boolean = false; + @state() + allowDownload: boolean = false; + @state() + refreshCount: number = 0; + + private retrievePageDataTask = new Task, Model>(this, + async ([publicIdentifier]) => { + const event = (await EventService.load(publicIdentifier)).event; + const dataTask = new Task, ListData>(this, async ([]) => { + const [items, count] = await Promise.all([AdditionalItemService.loadAll({eventId: event.id}), AdditionalItemService.useCount(event.id)]); + return { + items: items.filter(i => i.type === this.type), + usageCount: count, + allowDownload: Object.values(count).some(p => Object.values(p).reduce((pv: number, cv: number) => pv + cv) > 0), + } + }, () => [this.refreshCount]); + return { + event, + title: this.pageTitle ?? '', + icon: this.icon ?? '', + type: this.type!, + supportedLanguages: supportedLanguages(), + dataTask + }; + }, + () => [this.publicIdentifier!]); + + static styles = [pageHeader, textColors, css` + .item { + width: 100%; + margin-bottom: 1rem; + } + .item [slot='header'] { + display: flex; + align-items: center; + justify-content: space-between; + } + .item [slot='footer'] { + display: flex; + align-items: center; + justify-content: end; + gap: 1em; + } + + .item .body { + display: grid; + row-gap: 0.5rem; + } + + .item .body .info-container { + display: grid; + row-gap: 0.5rem; + } + + .item .body .info-container .info { + display: grid; + grid-template-columns: 0.5fr 1.3fr; + grid-auto-rows: auto; + column-gap: 3rem; + } + + + @media only screen and (min-width: 768px) { + .item > .body { + grid-template-columns: 1fr 1.3fr; + grid-auto-rows: auto; + column-gap: 3rem; + } + } + + sl-tab-group { + height: 100%; + } + + .panel-content { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 80px; + } + + .ps { + padding-left: 0.5rem; + } + + .page-header { + display: flex; + justify-content: space-between; + align-items: center; + } + `]; + + @query("alfio-additional-item-edit") + itemEditComponent?: AdditionalItemEdit; + + render() { + return this.retrievePageDataTask.render({ + initial: () => html`loading...`, + complete: (model) => html` + + + + + + ${this.iterateItems(model)} + + ${this.generateFooter(model)} + + ` + }); + } + + async addNew(model: Model): Promise { + if (this.itemEditComponent != null) { + this.editActive = await this.itemEditComponent.open({ + supportedLanguages: model.event.contentLanguages, + event: model.event, + type: this.type!, + editedItem: null + }); + } + } + + async edit(item: AdditionalItem, model: Model): Promise { + if (this.itemEditComponent != null) { + this.editActive = await this.itemEditComponent.open({ + supportedLanguages: model.event.contentLanguages, + event: model.event, + type: this.type!, + editedItem: item + }); + } + } + + async delete(item: AdditionalItem, model: Model): Promise { + try { + const confirmation = await ConfirmationDialogService.requestConfirm( + "Delete additional option?", + `Do you want to delete Additional option "${item.title.map(t => t.value).join("/")}"?`, + 'danger' + ); + if (confirmation) { + const response = await AdditionalItemService.deleteAdditionalItem(item.id, model.event.id); + if(response.ok) { + dispatchFeedback({ + type: 'success', + message: 'Additional Option successfully deleted' + }, this); + this.triggerListRefresh(); + } else { + dispatchFeedback({ + type: 'danger', + message: 'Cannot delete additional option' + }, this); + } + } + return false; + } catch(e) { + return false; + } + } + + private generateFooter(model: Model): TemplateResult { + const warning = () => html` +
+

Cannot add ${model.type === 'DONATION' ? 'donations' : 'additional options'} to an event marked as "free of charge".

+

Please change this setting, add a default price > 0, specify currency and Taxes

+
`; + const footer = () => html` +
+
+ this.addNew(model)} size="large"> + + Add new + +
+
`; + return when(model.event.freeOfCharge, warning, () => renderIf(() => !this.editActive, footer)); + } + + private iterateItems(model: Model) { + return model.dataTask.render({ + initial: () => html`loading...`, + complete: listData => html`${repeat(listData.items, (item) => item.id, (item) => { + return html` +
+ +
+
${showItemTitle(item)}
+
${`Confirmed: ${formatSoldCount(listData, item.id)}`}
+
+
+ this.edit(item, model)} type="button"> edit + ${renderIf(() => countUsage(listData, item.id) === 0, () => html` this.delete(item, model)} type="button"> delete`)} +
+
+
+
+ Inception + +
+
+ Expiration + +
+
+ Price + ${when(item.fixPrice, + () => this.showItemFixPrice(item), + () => html`User-defined`)} +
+ ${renderIf(() => item.type === 'SUPPLEMENT', () => html` +
+ Policy + ${supplementPolicyDescriptions[item.supplementPolicy]} +
`)} + + ${renderIf(() => item.fixPrice && (!isMandatory(item.supplementPolicy) && item.supplementPolicy !== 'OPTIONAL_UNLIMITED_AMOUNT'), + () => html` +
+ Max Qty per ${item.supplementPolicy === 'OPTIONAL_MAX_AMOUNT_PER_TICKET' ? 'ticket' : 'order'} + ${item.maxQtyPerOrder} +
`)} +
+ + + ${repeat(item.description, d => d.id, (d) => html` + ${d.locale} + +
+ +
${d.value}
+
+
+ + `)} + +
+
+
+ ` + })}` + }) + } + + private showItemFixPrice(item: AdditionalItem): TemplateResult { + if (isMandatoryPercentage(item.supplementPolicy)) { + return html`${item.price}%`; + } + return html``; + } + + private async editDialogClosed(e: AlfioDialogClosed) { + this.editActive = false; + if (e.detail.success) { + this.triggerListRefresh(); + dispatchFeedback({ + type: 'success', + message: 'Operation completed successfully' + }, this); + } + } + + private triggerListRefresh(): void { + this.refreshCount++; + } +} + +function countUsage(listData: ListData, itemId: number): number { + if (listData.usageCount[itemId] != null) { + const detail = listData.usageCount[itemId]; + const acquired = detail['ACQUIRED'] ?? 0; + const checkedIn = detail['CHECKED_IN'] ?? 0; + const toBePaid = detail['TO_BE_PAID'] ?? 0; + return acquired + checkedIn + toBePaid; + } + return 0; +} + +function formatSoldCount(listData: ListData, itemId: number): string { + if (listData.usageCount[itemId] != null) { + const detail = listData.usageCount[itemId]; + const acquired = detail['ACQUIRED'] ?? 0; + const checkedIn = detail['CHECKED_IN'] ?? 0; + const toBePaid = detail['TO_BE_PAID'] ?? 0; + const totalSold = acquired + checkedIn + toBePaid; + return totalSold + (checkedIn > 0 || toBePaid > 0 ? ` (of which ${acquired} Acquired, ${checkedIn} Checked in, ${toBePaid} To be paid on site)` : ''); + } + return '0'; +} + +function showItemTitle(item: AdditionalItem): TemplateResult { + return html`${repeat(item.title, title => title.id, (title, index) => { + return html` + + ${ title.value !== '' ? title.value : `!! missing ${title.locale} !!` } + ${when(index + 1 < item.title.length, () => html` / `, () => nothing)} + ` + })}`; +} + +declare global { + interface HTMLElementTagNameMap { + 'alfio-additional-item-list': AdditionalItemList + } +} diff --git a/frontend/admin/src/feedback-visualizer/feedback-visualizer.ts b/frontend/admin/src/feedback-visualizer/feedback-visualizer.ts new file mode 100644 index 0000000000..a7d9edcf2b --- /dev/null +++ b/frontend/admin/src/feedback-visualizer/feedback-visualizer.ts @@ -0,0 +1,57 @@ +import {customElement} from "lit/decorators.js"; +import {html, LitElement, TemplateResult} from "lit"; +import {AlfioFeedbackEvent} from "../model/dom-events.ts"; +import {escapeHtml} from "../service/helpers.ts"; + +@customElement('alfio-feedback-visualizer') +export class FeedbackVisualizer extends LitElement { + + private listener: EventListener = (e) => { + const detail = (e as CustomEvent).detail; + const alert = Object.assign(document.createElement('sl-alert'), { + variant: detail.type, + closable: true, + duration: 3000, + innerHTML: ` + + ${escapeHtml(detail.message)} + ` + }); + + document.body.append(alert); + return alert.toast(); + }; + + private getIcon(detail: AlfioFeedbackEvent): string { + switch(detail.type) { + case "success": + return 'check2-circle'; + case "warning": + return 'exclamation-triangle'; + case 'danger': + return 'exclamation-octagon'; + default: + return 'info-circle'; + } + } + + protected render(): TemplateResult { + return html` + + + Your account has been deleted
+ We're very sorry to see you go! +
+ `; + } + + connectedCallback() { + super.connectedCallback(); + window.addEventListener("alfio-feedback", this.listener); + } + + disconnectedCallback() { + super.disconnectedCallback(); + window.removeEventListener("alfio-feedback", this.listener); + } +} diff --git a/frontend/admin/src/main.css b/frontend/admin/src/main.css index 0b50d95d28..02cdc15675 100644 --- a/frontend/admin/src/main.css +++ b/frontend/admin/src/main.css @@ -1,3 +1,12 @@ :root { font-size: 16px; /* reset bootstrap css, set default font size as defined in https://shoelace.style/tokens/typography#font-size */ -} \ No newline at end of file + --sl-font-sans: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif; + --sl-color-danger-600: var(--sl-color-red-700); +} + +.sl-toast-stack { + bottom: 16px; + right: 16px; + z-index: 9999; + top: auto; +} diff --git a/frontend/admin/src/main.ts b/frontend/admin/src/main.ts index 6bc76be7a5..dae248e662 100644 --- a/frontend/admin/src/main.ts +++ b/frontend/admin/src/main.ts @@ -1,16 +1,28 @@ -// see +// see // https://github.com/shoelace-style/rollup-example/blob/master/src/index.js // + // https://shoelace.style/getting-started/installation?id=bundling#bundling -import './main.css'; import '@shoelace-style/shoelace/dist/themes/light.css'; +import './main.css'; import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path.js'; +import "@shoelace-style/shoelace/dist/components/format-date/format-date.js"; +import "@shoelace-style/shoelace/dist/components/format-number/format-number.js"; +import "@shoelace-style/shoelace/dist/components/divider/divider.js"; +import "@shoelace-style/shoelace/dist/components/dialog/dialog.js"; +import "@shoelace-style/shoelace/dist/components/button/button.js"; +import "@shoelace-style/shoelace/dist/components/tab-group/tab-group.js"; +import "@shoelace-style/shoelace/dist/components/tab/tab.js"; +import "@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js"; // imported components import '@shoelace-style/shoelace/dist/components/card/card.js'; import '@shoelace-style/shoelace/dist/components/button/button.js'; import '@shoelace-style/shoelace/dist/components/icon/icon.js'; import '@shoelace-style/shoelace/dist/components/alert/alert'; +import '@shoelace-style/shoelace/dist/components/input/input'; +import '@shoelace-style/shoelace/dist/components/textarea/textarea'; +import '@shoelace-style/shoelace/dist/components/select/select'; +import '@shoelace-style/shoelace/dist/components/option/option'; @@ -22,4 +34,8 @@ if (import.meta.env.MODE === 'development') { setBasePath(`${document.getElementById('lit-js')?.getAttribute('src')?.replace(/assets.*/g, '')}shoelace/`); } export { NewElement } from './new-element'; -export { ProjectBanner } from './project-banner/project-banner'; \ No newline at end of file +export { ProjectBanner } from './project-banner/project-banner'; +export { AdditionalItemList } from './event/additional-item-list/additional-item-list'; +export { DisplayCommonMarkPreview } from './display-common-mark-preview/display-common-mark-preview'; +export { AdditionalItemEdit } from './event/additional-item-edit/additional-item-edit'; +export { FeedbackVisualizer } from './feedback-visualizer/feedback-visualizer'; diff --git a/frontend/admin/src/model/additional-item.ts b/frontend/admin/src/model/additional-item.ts new file mode 100644 index 0000000000..8854f6bc91 --- /dev/null +++ b/frontend/admin/src/model/additional-item.ts @@ -0,0 +1,69 @@ +import {DateTimeModification} from "./event.ts"; + +export type AdditionalItemType = 'DONATION' | 'SUPPLEMENT'; +export type AdditionalItemTaxType = 'INHERITED' | 'NONE' | 'CUSTOM_INCLUDED' | 'CUSTOM_EXCLUDED'; +export type SupplementPolicy = + 'MANDATORY_ONE_FOR_TICKET' | + 'MANDATORY_PERCENTAGE_RESERVATION' | + 'MANDATORY_PERCENTAGE_FOR_TICKET' | + 'OPTIONAL_UNLIMITED_AMOUNT' | + 'OPTIONAL_MAX_AMOUNT_PER_TICKET' | + 'OPTIONAL_MAX_AMOUNT_PER_RESERVATION'; + +// temporary until we get the i18n translations +export const supplementPolicyDescriptions: {[key: string]: string} = { + 'MANDATORY_ONE_FOR_TICKET': 'Mandatory fixed fee, 1 per ticket', + 'MANDATORY_PERCENTAGE_RESERVATION': 'Mandatory percentage fee, entire reservation (including user-selected Additional Items, if any)', + 'MANDATORY_PERCENTAGE_FOR_TICKET': 'Mandatory percentage fee for tickets', + 'OPTIONAL_UNLIMITED_AMOUNT': 'User-selected item', + 'OPTIONAL_MAX_AMOUNT_PER_TICKET': 'User-selected item, limited quantity per ticket', + 'OPTIONAL_MAX_AMOUNT_PER_RESERVATION': 'User-selected item, limited quantity per reservation', +} + +export function isMandatory(supplementPolicy: SupplementPolicy): boolean { + return supplementPolicy === "MANDATORY_ONE_FOR_TICKET" + || supplementPolicy === 'MANDATORY_PERCENTAGE_FOR_TICKET' + || supplementPolicy === 'MANDATORY_PERCENTAGE_RESERVATION'; +} + +export function isMandatoryPercentage(supplementPolicy: SupplementPolicy): boolean { + return supplementPolicy === 'MANDATORY_PERCENTAGE_FOR_TICKET' + || supplementPolicy === 'MANDATORY_PERCENTAGE_RESERVATION'; +} + +export const taxTypeDescriptions: {[key: string]: string} = { + 'INHERITED': 'Use Event settings', + 'NONE': 'Do not apply taxes on this items' +} + +// export type AdditionalItemStatus = 'FREE' | 'PENDING' | 'TO_BE_PAID' | 'ACQUIRED' | 'CANCELLED' | 'CHECKED_IN' | 'EXPIRED' | 'INVALIDATED' | 'RELEASED'; + +export interface AdditionalItemLocalizedContent { + id: number | null, + locale: string, + value: string, + type: 'DESCRIPTION' | 'TITLE' +} + +export interface AdditionalItem { + id: number; + price: number, + fixPrice: boolean, + ordinal: number, + availableQuantity?: number, + maxQtyPerOrder?: number, + inception: DateTimeModification, + expiration: DateTimeModification, + vat: number | null, + vatType: AdditionalItemTaxType, + title: AdditionalItemLocalizedContent[], + description: AdditionalItemLocalizedContent[], + type: AdditionalItemType, + supplementPolicy: SupplementPolicy, + currencyCode: string, + availableItems: number | null, + minPrice: number | null, + maxPrice: number | null, + finalPrice: number, + currency: string +} diff --git a/frontend/admin/src/model/dom-events.ts b/frontend/admin/src/model/dom-events.ts new file mode 100644 index 0000000000..61bf560ebf --- /dev/null +++ b/frontend/admin/src/model/dom-events.ts @@ -0,0 +1,24 @@ +import {LitElement} from "lit"; + +export type AlfioDialogClosed = CustomEvent<{ success: boolean }>; +export type AlfioFeedback = CustomEvent; + +export interface AlfioFeedbackEvent { + type: 'neutral' | 'success' | 'warning' | 'danger'; + message: string; +} + +export function dispatchFeedback(payload: AlfioFeedbackEvent, src: LitElement): void { + src.dispatchEvent(new CustomEvent('alfio-feedback', { + detail: payload, + bubbles: true, + composed: true + })); +} + +declare global { + interface GlobalEventHandlersEventMap { + 'alfio-dialog-closed': AlfioDialogClosed; + 'alfio-feedback': AlfioFeedback; + } +} diff --git a/frontend/admin/src/model/event.ts b/frontend/admin/src/model/event.ts new file mode 100644 index 0000000000..5064cc1928 --- /dev/null +++ b/frontend/admin/src/model/event.ts @@ -0,0 +1,67 @@ +import {Organization} from "./organization.ts"; + +export interface EventWithOrganization { + event: AlfioEvent; + organization: Organization; +} + +export interface DateTimeModification { + date: string; + time: string; +} + +export interface AlfioEvent { + id: number; + shortName: string; + displayName: string; + publicIdentifier: string; + ticketCategories: TicketCategory[]; + description: LocalizedContent; + title: LocalizedContent; + begin: string; + format: string; + currency: string; + formattedBegin: string; + visibleForCurrentUser: boolean; + displayStatistics: boolean; + status: string; + expired: boolean; + locales: number; + freeOfCharge: boolean; + sameDay: boolean; + end: string; + online: boolean; + organizationId: number; + regularPrice: number; + contentLanguages: ContentLanguage[]; + termsAndConditionsUrl: string; + privacyPolicyUrl: string; + vatIncluded: boolean; + vatPercentage: number; + beginTimeZoneOffset: number; + endTimeZoneOffset: number; + isOnline: boolean; + firstContentLanguage: ContentLanguage; + supportsAdditionalItemsQuantity: boolean; + finalPrice: number; + netPrice: number; + taxablePrice: number; +} + +export interface ContentLanguage { + locale: string; + value: number; + language: string; + displayLanguage: string; +} + +export interface LocalizedContent { + [key: string]: string; +} + +export interface TicketCategory { + description: LocalizedContent; + name: string; + id: number; +} + diff --git a/frontend/admin/src/model/organization.ts b/frontend/admin/src/model/organization.ts new file mode 100644 index 0000000000..876953d673 --- /dev/null +++ b/frontend/admin/src/model/organization.ts @@ -0,0 +1,7 @@ +export interface Organization { + id: number; + name: string; + email: string; + externalId: string | null; + slug: string | null; +} diff --git a/frontend/admin/src/model/validation.ts b/frontend/admin/src/model/validation.ts new file mode 100644 index 0000000000..bb89020962 --- /dev/null +++ b/frontend/admin/src/model/validation.ts @@ -0,0 +1,18 @@ +export interface ValidatedResponse { + success: boolean; + errorCount: number; + validationErrors: ErrorDescriptor[]; + value: T; + warnings: Array; +} + +export interface ErrorDescriptor { + fieldName: string; + code: string; + arguments: {[key: string]: any}; +} + +export interface WarningMessage { + code: string; + params: Array; +} diff --git a/frontend/admin/src/service/additional-item.ts b/frontend/admin/src/service/additional-item.ts new file mode 100644 index 0000000000..bb126f2ba2 --- /dev/null +++ b/frontend/admin/src/service/additional-item.ts @@ -0,0 +1,33 @@ +import {AdditionalItem} from "../model/additional-item.ts"; +import {ValidatedResponse} from "../model/validation.ts"; +import {callDelete, fetchJson, postJson, putJson} from "./helpers.ts"; + +export type UsageCount = { [id: number]: { [ status: string ]: number } }; + +export class AdditionalItemService { + static async loadAll({eventId}: { eventId: number }): Promise> { + return await fetchJson(`/admin/api/event/${eventId}/additional-services`); + } + + static async useCount(eventId: number): Promise { + return await fetchJson(`/admin/api/event/${eventId}/additional-services/count`); + } + + static async validateAdditionalItem(additionalItem: Partial): Promise> { + const response = await postJson('/admin/api/additional-services/validate', additionalItem); + return response.json(); + } + + static async updateAdditionalItem(additionalItem: Partial, eventId: number): Promise { + if (additionalItem.id != null) { + return await putJson(`/admin/api/event/${eventId}/additional-services/${additionalItem.id}`, additionalItem); + } + return await postJson(`/admin/api/event/${eventId}/additional-services`, additionalItem); + } + + static async deleteAdditionalItem(additionalItemId: number, eventId: number): Promise { + return await callDelete(`/admin/api/event/${eventId}/additional-services/${additionalItemId}`) + } + +} + diff --git a/frontend/admin/src/service/confirmation-dialog.ts b/frontend/admin/src/service/confirmation-dialog.ts new file mode 100644 index 0000000000..e55d7571bc --- /dev/null +++ b/frontend/admin/src/service/confirmation-dialog.ts @@ -0,0 +1,43 @@ +import {escapeHtml} from "./helpers.ts"; + +export class ConfirmationDialogService { + public static requestConfirm(title: string, + message: string, + variant: 'success' | 'danger' | 'warning' | 'primary' = 'success'): Promise { + return new Promise((resolve) => { + const div = document.createElement('div'); + div.innerHTML = ` + +
${escapeHtml(message)}
+
+ Close + Confirm +
+
+ `; + const dialog = div.querySelector('sl-dialog')!; + + document.body.appendChild(div); + + const close = (r: boolean) => { + dialog.hide().then(() => { + document.body.removeChild(div); + resolve(r); + }); + } + customElements.whenDefined('sl-dialog').then(async () => { + await dialog.show(); + div.querySelectorAll('sl-button')?.forEach(e => e.addEventListener('click', () => { + close(e.id === 'confirm') + })); + dialog.addEventListener('sl-hide', (e) => { + if (['close-button', 'keyboard', 'overlay'].includes(e.detail.source)) { + resolve(false); + } + }); + }); + }) + } + + +} diff --git a/frontend/admin/src/service/event.ts b/frontend/admin/src/service/event.ts new file mode 100644 index 0000000000..c10c96d1e4 --- /dev/null +++ b/frontend/admin/src/service/event.ts @@ -0,0 +1,8 @@ +import {EventWithOrganization} from "../model/event.ts"; +import {fetchJson} from "./helpers.ts"; + +export class EventService { + static load(publicIdentifier: string): Promise { + return fetchJson(`/admin/api/events/${publicIdentifier}`); + } +} diff --git a/frontend/admin/src/service/helpers.ts b/frontend/admin/src/service/helpers.ts index 8ccf020cca..6d9c14d389 100644 --- a/frontend/admin/src/service/helpers.ts +++ b/frontend/admin/src/service/helpers.ts @@ -1,13 +1,81 @@ +import {ContentLanguage, DateTimeModification} from "../model/event.ts"; +import {html, nothing, TemplateResult} from "lit"; +import {when} from "lit/directives/when.js"; +import {FieldApi} from "@tanstack/lit-form"; + export function postJson(url: string, payload: any): Promise { + return performRequest(url, 'POST', payload); +} + +export function putJson(url: string, payload: any): Promise { + return performRequest(url, 'PUT', payload); +} + +export function callDelete(url: string): Promise { + return performRequest(url, 'DELETE', null); +} + +function performRequest(url: string, method: 'PUT' | 'POST' | 'DELETE', payload: any): Promise { const xsrfName = document.querySelector('meta[name=_csrf_header]')?.getAttribute('content') as string; const xsrfValue = document.querySelector('meta[name=_csrf]')?.getAttribute('content') as string; return fetch(url, { - method: 'POST', credentials: 'include', headers: { + method, credentials: 'include', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', [xsrfName]: xsrfValue }, - body: JSON.stringify(payload) + body: payload != null ? JSON.stringify(payload) : null }) -} \ No newline at end of file +} + +export function fetchJson(url: string) : Promise { + return fetch(url, { + method: 'GET', + credentials: 'include' + }).then(r => r.json()); +} + +export function renderIf(predicate: () => boolean, template: () => TemplateResult): TemplateResult { + return html`${when(predicate(), template, () => nothing)}`; +} + +export function supportedLanguages(): ContentLanguage[] { + if (window.SUPPORTED_LANGUAGES != null) { + return JSON.parse(window.SUPPORTED_LANGUAGES); + } + return []; +} + +export function toDateTimeModification(isoString: string): DateTimeModification { + return { + date: isoString.substring(0, 10), + time: isoString.substring(11, 16), + }; +} + +export function extractDateTime(isoString?: string): string { + if (isoString != null) { + return isoString.substring(0, 16); + } + return ""; +} + +export function notifyChange(event: InputEvent, field: FieldApi): void { + const target = event.currentTarget as HTMLInputElement | null; + if (target != null) { + field.handleChange(target.value); + } +} + +export function escapeHtml(message: string): string { + const div = document.createElement('div'); + div.textContent = message; + return div.innerHTML; +} + +declare global { + interface Window { + SUPPORTED_LANGUAGES: string | null; + } +} diff --git a/frontend/admin/src/service/util.ts b/frontend/admin/src/service/util.ts new file mode 100644 index 0000000000..80ec871c79 --- /dev/null +++ b/frontend/admin/src/service/util.ts @@ -0,0 +1,7 @@ +import {fetchJson} from "./helpers.ts"; + +export class UtilService { + static renderMarkdown(text: string): Promise { + return fetchJson(`/admin/api/utils/render-commonmark?text=${encodeURIComponent(text)}`) + } +} diff --git a/frontend/admin/src/styles.ts b/frontend/admin/src/styles.ts new file mode 100644 index 0000000000..70a7f064ec --- /dev/null +++ b/frontend/admin/src/styles.ts @@ -0,0 +1,83 @@ +import {css} from "lit"; + +export const pageHeader = css` + .page-header { + padding-bottom: 10px; + margin: 44px 0 22px; + border-bottom: 1px solid #eee; + } + + .border-bottom { + border-bottom: 1px solid #eee; + margin-bottom: 15px; + } +`; + +export const textColors = css` + .text-success { + color: var(--sl-color-success-600); + } + + .text-muted { + color: var(--sl-color-gray-600); + } + + .text-danger { + color: var(--sl-color-danger-600); + } +` + + + +export const row = css` + + :host { + --alfio-row-cols: 2 + } + + .row { + display: grid; + row-gap: 0.5rem; + } + + @media only screen and (min-width: 768px) { + .row { + grid-template-columns: repeat(var(--alfio-row-cols), 1fr); + grid-auto-rows: auto; + column-gap: 3rem; + } + } +`; + +export const dialog = css` + :host { + --sl-z-index-dialog: 1031; // bootstrap's navbar + 1 + } +`; + +export const form = css` + sl-input::part(form-control-label), + sl-textarea::part(form-control-label), + sl-select::part(form-control-label){ + font-weight: bold; + } + + sl-input, sl-textarea, sl-select { + margin-top: 15px; + } + + sl-input.error, sl-textarea.error, sl-select.error { + --sl-input-border-color: var(--sl-color-danger-600); + --sl-input-border-color-hover: var(--sl-color-danger-500); + --sl-input-border-color-focus: var(--sl-color-danger-600); + --sl-input-focus-ring-color: var(--sl-color-danger-200); + } + + .error-text { + display: none; + } + + sl-input.error .error-text, sl-textarea.error .error-text, sl-select.error .error-text { + display: inline-block; + } +`; diff --git a/frontend/projects/public/src/app/additional-service-form/additional-service-form.component.html b/frontend/projects/public/src/app/additional-service-form/additional-service-form.component.html index e29677f118..f5ecc498de 100644 --- a/frontend/projects/public/src/app/additional-service-form/additional-service-form.component.html +++ b/frontend/projects/public/src/app/additional-service-form/additional-service-form.component.html @@ -1,25 +1,29 @@ - -
-

-
+@if (additionalServices.length > 0) { + @if (hasSupplements) { +
+

+
+ }
-
-
-
-

{{ asw.title | translateDescription}}

-
- -
- + @for (asw of additionalServices; track asw.itemId; let idx = $index; let last = $last) { + @if (asw.type === 'SUPPLEMENT') { +
+
+

{{ asw.title | translateDescription}}

+
+ +
+ +
+
+
+
+
-
-
- -
-
-
+ } + }
- +} diff --git a/frontend/projects/public/src/app/additional-service-quantity-selector/additional-service-quantity-selector.html b/frontend/projects/public/src/app/additional-service-quantity-selector/additional-service-quantity-selector.html index 8edc795455..ca5b2bd0da 100644 --- a/frontend/projects/public/src/app/additional-service-quantity-selector/additional-service-quantity-selector.html +++ b/frontend/projects/public/src/app/additional-service-quantity-selector/additional-service-quantity-selector.html @@ -1,20 +1,21 @@
-
-
+ @if (additionalService.fixPrice && additionalService.supplementPolicy === 'MANDATORY_ONE_FOR_TICKET') { +
+ } @else if (additionalService.supplementPolicy === 'MANDATORY_PERCENTAGE_FOR_TICKET' || additionalService.supplementPolicy === 'MANDATORY_PERCENTAGE_RESERVATION') { +
+ } @else if (additionalService.fixPrice && additionalService.supplementPolicy === 'OPTIONAL_UNLIMITED_AMOUNT') { -
-
+ } @else if (additionalService.fixPrice && [null, 'OPTIONAL_MAX_AMOUNT_PER_RESERVATION', 'OPTIONAL_MAX_AMOUNT_PER_TICKET'].indexOf(additionalService.supplementPolicy) >= 0) { -
-
+ } @else if (!additionalService.fixPrice) {
{{event.currency}}
-
+ }
diff --git a/frontend/projects/public/src/app/additional-service/additional-service.component.html b/frontend/projects/public/src/app/additional-service/additional-service.component.html index 1e9a5706cd..d11a39f2c8 100644 --- a/frontend/projects/public/src/app/additional-service/additional-service.component.html +++ b/frontend/projects/public/src/app/additional-service/additional-service.component.html @@ -2,10 +2,24 @@ {{additionalService.title[translate.currentLang]}}
- - - - + @if (additionalService.free) { + + } @else if (!additionalService.free && additionalService.fixPrice) { + + @if (mandatoryPercentage) { +
+
+ {{additionalService.formattedFinalPrice}}% +
+
+ {{( additionalService.supplementPolicy === 'MANDATORY_PERCENTAGE_FOR_TICKET' ? 'show-event.additional.percentage.tickets' : 'show-event.additional.percentage.reservation') | translate }} +
+
+ } @else { + + } +
+ }
diff --git a/frontend/projects/public/src/app/additional-service/additional-service.component.ts b/frontend/projects/public/src/app/additional-service/additional-service.component.ts index 77cca590ee..b123fead94 100644 --- a/frontend/projects/public/src/app/additional-service/additional-service.component.ts +++ b/frontend/projects/public/src/app/additional-service/additional-service.component.ts @@ -68,6 +68,11 @@ export class AdditionalServiceComponent implements OnInit, OnDestroy { } } + get mandatoryPercentage(): boolean { + return this.additionalService.supplementPolicy === 'MANDATORY_PERCENTAGE_RESERVATION' + || this.additionalService.supplementPolicy === 'MANDATORY_PERCENTAGE_FOR_TICKET'; + } + public ngOnDestroy(): void { if (this.formSub) { this.formSub.unsubscribe(); diff --git a/frontend/projects/public/src/app/model/additional-service.ts b/frontend/projects/public/src/app/model/additional-service.ts index f4211177e3..28a426a5ce 100644 --- a/frontend/projects/public/src/app/model/additional-service.ts +++ b/frontend/projects/public/src/app/model/additional-service.ts @@ -27,5 +27,10 @@ export class AdditionalService { } export type AdditionalServiceType = 'DONATION' | 'SUPPLEMENT'; -export type SupplementPolicy = 'MANDATORY_ONE_FOR_TICKET' | 'OPTIONAL_UNLIMITED_AMOUNT' | 'OPTIONAL_MAX_AMOUNT_PER_TICKET' | - 'OPTIONAL_MAX_AMOUNT_PER_RESERVATION'; +export type SupplementPolicy = + 'MANDATORY_ONE_FOR_TICKET' | + 'MANDATORY_PERCENTAGE_RESERVATION' | + 'MANDATORY_PERCENTAGE_FOR_TICKET' | + 'OPTIONAL_UNLIMITED_AMOUNT' | + 'OPTIONAL_MAX_AMOUNT_PER_TICKET' | + 'OPTIONAL_MAX_AMOUNT_PER_RESERVATION'; diff --git a/src/main/java/alfio/controller/AdminIndexController.java b/src/main/java/alfio/controller/AdminIndexController.java index 7c983be2d2..89310bef42 100644 --- a/src/main/java/alfio/controller/AdminIndexController.java +++ b/src/main/java/alfio/controller/AdminIndexController.java @@ -18,11 +18,16 @@ import alfio.config.authentication.support.OpenIdAlfioAuthentication; import alfio.controller.support.CSPConfigurer; +import alfio.manager.i18n.I18nManager; import alfio.manager.system.ConfigurationManager; import alfio.model.user.Role; +import alfio.util.Json; import alfio.util.TemplateManager; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.samskivert.mustache.Mustache; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.env.Environment; import org.springframework.core.io.ClassPathResource; @@ -32,13 +37,12 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.security.Principal; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.function.Function; import static alfio.controller.Constants.*; import static alfio.model.system.ConfigurationKeys.SHOW_PROJECT_BANNER; @@ -54,16 +58,19 @@ public record ManifestEntry(List css, String file) { private final CSPConfigurer cspConfigurer; private final TemplateManager templateManager; private final ManifestEntry manifestEntry; + private final I18nManager i18nManager; public AdminIndexController(ConfigurationManager configurationManager, Environment environment, CSPConfigurer cspConfigurer, TemplateManager templateManager, - ObjectMapper objectMapper) throws IOException { + ObjectMapper objectMapper, + I18nManager i18nManager) throws IOException { this.configurationManager = configurationManager; this.environment = environment; this.cspConfigurer = cspConfigurer; this.templateManager = templateManager; + this.i18nManager = i18nManager; var cpr = new ClassPathResource("/resources/alfio-admin-frontend/.vite/manifest.json"); if (cpr.exists()) { try (var descriptor = cpr.getInputStream()) { @@ -95,6 +102,7 @@ public void adminHome(Model model, @Value("${alfio.version}") String version, Ht boolean isAdmin = authorities.contains(Role.ADMIN.getRoleName()); model.addAttribute("isOwner", isAdmin || authorities.contains(Role.OWNER.getRoleName())); model.addAttribute("isAdmin", isAdmin); + model.addAttribute("supportedLanguages", i18nManager.getAvailableLanguages()); // addCommonModelAttributes(model, request, version, environment); model.addAttribute("displayProjectBanner", isAdmin && configurationManager.getForSystem(SHOW_PROJECT_BANNER).getValueAsBooleanOrDefault()); @@ -111,7 +119,27 @@ public void adminHome(Model model, @Value("${alfio.version}") String version, Ht response.setCharacterEncoding(UTF_8); var nonce = cspConfigurer.addCspHeader(response, false); model.addAttribute(NONCE, nonce); + model.addAttribute("render-json", RENDER_JSON.apply(model.asMap())); templateManager.renderHtml(new ClassPathResource("alfio/web-templates/admin-index.ms"), model.asMap(), os); } } + + /** + * {{#render-json}}property-to-render{{/render-json}} + * If the property does not exist, we render it as 'null' + */ + static final Function, Mustache.Lambda> RENDER_JSON = model -> (frag, out) -> { + if (model == null) { + out.write("null"); + return; + } + + var property = frag.execute().strip(); + + if (model.containsKey(property)) { + out.write("'" + Json.toJson(model.get(property)) + "'"); + } else { + out.write("null"); + } + }; } diff --git a/src/main/java/alfio/controller/api/admin/AdditionalServiceApiController.java b/src/main/java/alfio/controller/api/admin/AdditionalServiceApiController.java index 515769914f..1e46133a13 100644 --- a/src/main/java/alfio/controller/api/admin/AdditionalServiceApiController.java +++ b/src/main/java/alfio/controller/api/admin/AdditionalServiceApiController.java @@ -30,7 +30,6 @@ import alfio.util.MonetaryUtil; import alfio.util.Validator; import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -86,7 +85,7 @@ public List loadAll(@PathVariable int event .map(event -> additionalServiceManager.loadAllForEvent(eventId) .stream() .map(as -> EventModification.AdditionalService.from(as)//.withAdditionalFields() TODO to be implemented - .withText(additionalServiceManager.findAllTextByAdditionalServiceId(as.getId())) + .withText(additionalServiceManager.findAllTextByAdditionalServiceId(as.id())) .withZoneId(event.getZoneId()) .withPriceContainer(buildPriceContainer(event, as)).build()) .collect(Collectors.toList())) @@ -107,7 +106,7 @@ public ResponseEntity update(@PathVariable Optional optional = additionalServiceManager.getOptionalById(additionalServiceId, eventId); Assert.isTrue(optional.isPresent(), "No additional service with id " + additionalServiceId + " present in eventId " + eventId); var existing = optional.get(); - Assert.isTrue(existing.getAvailableQuantity() == -1 || additionalService.getAvailableQuantity() > 0, "Missing available quantity"); + Assert.isTrue(existing.availableQuantity() == -1 || additionalService.getAvailableQuantity() > 0, "Missing available quantity"); // ValidationResult validationResult = Validator.validateAdditionalService(additionalService, bindingResult); Validate.isTrue(validationResult.isSuccess(), "validation failed"); @@ -216,7 +215,7 @@ private static PriceContainer buildPriceContainer(final Event event, final Addit return new PriceContainer() { @Override public int getSrcPriceCts() { - return as.isFixPrice() ? as.getSrcPriceCts() : 0; + return as.fixPrice() ? as.srcPriceCts() : 0; } @Override @@ -231,7 +230,7 @@ public Optional getOptionalVatPercentage() { @Override public VatStatus getVatStatus() { - return AdditionalService.getVatStatus(as.getVatType(), event.getVatStatus()); + return AdditionalService.getVatStatus(as.vatType(), event.getVatStatus()); } }; } diff --git a/src/main/java/alfio/controller/api/v2/model/ReservationInfo.java b/src/main/java/alfio/controller/api/v2/model/ReservationInfo.java index 6db16847c1..4dfeefaa94 100644 --- a/src/main/java/alfio/controller/api/v2/model/ReservationInfo.java +++ b/src/main/java/alfio/controller/api/v2/model/ReservationInfo.java @@ -36,7 +36,6 @@ import java.util.Map; import java.util.Objects; import java.util.UUID; -import java.util.stream.Collectors; @AllArgsConstructor @Getter @@ -114,7 +113,7 @@ public ReservationInfoOrderSummary(OrderSummary orderSummary) { this.summary = orderSummary.getSummary() .stream() .map(s -> new ReservationInfoOrderSummaryRow(s.getName(), s.getAmount(), s.getPrice(), s.getSubTotal(), s.getType(), s.getTaxPercentage())) - .collect(Collectors.toList()); + .toList(); this.totalPrice = orderSummary.getTotalPrice(); this.free = orderSummary.getFree(); this.displayVat = orderSummary.getDisplayVat(); @@ -156,11 +155,11 @@ public SubscriptionInfo(UUID id, String pin, UsageDetails usageDetails, Subscrip } public List getFieldConfigurationBeforeStandard() { - return additionalFields.stream().filter(AdditionalField::isBeforeStandardFields).collect(Collectors.toList()); + return additionalFields.stream().filter(AdditionalField::isBeforeStandardFields).toList(); } public List getFieldConfigurationAfterStandard() { - return additionalFields.stream().filter(tv -> !tv.isBeforeStandardFields()).collect(Collectors.toList()); + return additionalFields.stream().filter(tv -> !tv.isBeforeStandardFields()).toList(); } } diff --git a/src/main/java/alfio/controller/api/v2/user/EventApiV2Controller.java b/src/main/java/alfio/controller/api/v2/user/EventApiV2Controller.java index 3f33f96640..b7a92af7b8 100644 --- a/src/main/java/alfio/controller/api/v2/user/EventApiV2Controller.java +++ b/src/main/java/alfio/controller/api/v2/user/EventApiV2Controller.java @@ -204,17 +204,17 @@ public ResponseEntity getTicketCategories(@PathVariable String .collect(Collectors.toList()); // will be used for fetching descriptions and titles for all the languages - var saleableAdditionalServicesIds = saleableAdditionalServices.stream().map(SaleableAdditionalService::getId).collect(Collectors.toList()); + var saleableAdditionalServicesIds = saleableAdditionalServices.stream().map(SaleableAdditionalService::id).collect(Collectors.toList()); var additionalServiceTexts = additionalServiceManager.getDescriptionsByAdditionalServiceIds(saleableAdditionalServicesIds); var additionalServicesRes = saleableAdditionalServices.stream().map(as -> { var expiration = Formatters.getFormattedDate(event, as.getZonedExpiration(), "common.ticket-category.date-format", messageSource); var inception = Formatters.getFormattedDate(event, as.getZonedInception(), "common.ticket-category.date-format", messageSource); - var title = additionalServiceTexts.getOrDefault(as.getId(), Collections.emptyMap()).getOrDefault(AdditionalServiceText.TextType.TITLE, Collections.emptyMap()); - var description = Formatters.applyCommonMark(additionalServiceTexts.getOrDefault(as.getId(), Collections.emptyMap()).getOrDefault(AdditionalServiceText.TextType.DESCRIPTION, Collections.emptyMap()), messageSource); - return new AdditionalService(as.getId(), as.getType(), as.getSupplementPolicy(), - as.isFixPrice(), as.getAvailableItems(), as.getMaxQtyPerOrder(), + var title = additionalServiceTexts.getOrDefault(as.id(), Collections.emptyMap()).getOrDefault(AdditionalServiceText.TextType.TITLE, Collections.emptyMap()); + var description = Formatters.applyCommonMark(additionalServiceTexts.getOrDefault(as.id(), Collections.emptyMap()).getOrDefault(AdditionalServiceText.TextType.DESCRIPTION, Collections.emptyMap()), messageSource); + return new AdditionalService(as.id(), as.type(), as.supplementPolicy(), + as.fixPrice(), as.availableItems(), as.maxQtyPerOrder(), as.getFree(), as.getFormattedFinalPrice(), as.getSupportsDiscount(), as.getDiscountedPrice(), as.getVatApplies(), as.getVatIncluded(), as.getVatPercentage().toString(), as.isExpired(), as.getSaleInFuture(), inception, expiration, title, description); diff --git a/src/main/java/alfio/controller/decorator/SaleableAdditionalService.java b/src/main/java/alfio/controller/decorator/SaleableAdditionalService.java index 2b563c36d4..6cc9b74e4c 100644 --- a/src/main/java/alfio/controller/decorator/SaleableAdditionalService.java +++ b/src/main/java/alfio/controller/decorator/SaleableAdditionalService.java @@ -28,6 +28,8 @@ import java.time.ZonedDateTime; import java.util.Optional; +import static alfio.util.MonetaryUtil.formatPercentage; + public class SaleableAdditionalService implements PriceContainer { private final Event event; @Delegate(excludes = {Exclusions.class, PriceContainer.class}) @@ -45,7 +47,7 @@ public SaleableAdditionalService(Event event, } public boolean isExpired() { - return getUtcExpiration().isBefore(ZonedDateTime.now(clock)); + return utcExpiration().isBefore(ZonedDateTime.now(clock)); } public boolean getExpired() { @@ -53,11 +55,11 @@ public boolean getExpired() { } public boolean getSaleInFuture() { - return getUtcInception().isAfter(ZonedDateTime.now(clock)); + return utcInception().isAfter(ZonedDateTime.now(clock)); } public boolean getFree() { - return isFixPrice() && getFinalPrice().compareTo(BigDecimal.ZERO) == 0; + return fixPrice() && getFinalPrice().compareTo(BigDecimal.ZERO) == 0; } public ZonedDateTime getZonedInception() { @@ -70,13 +72,13 @@ public ZonedDateTime getZonedExpiration() { @Override public int getSrcPriceCts() { - return Optional.ofNullable(additionalService.getSrcPriceCts()).orElse(0); + return Optional.ofNullable(additionalService.srcPriceCts()).orElse(0); } @Override public Optional getDiscount() { return Optional.ofNullable(promoCodeDiscount) - .filter(x -> x.getCodeType() == PromoCodeDiscount.CodeType.DISCOUNT && getType() != AdditionalService.AdditionalServiceType.DONATION); + .filter(x -> x.getCodeType() == PromoCodeDiscount.CodeType.DISCOUNT && type() != AdditionalService.AdditionalServiceType.DONATION); } @Override @@ -94,15 +96,18 @@ public Optional getOptionalVatPercentage() { @Override public VatStatus getVatStatus() { - return AdditionalService.getVatStatus(getVatType(), event.getVatStatus()); + return AdditionalService.getVatStatus(vatType(), event.getVatStatus()); } public String getFormattedFinalPrice() { + if (AdditionalService.SupplementPolicy.isMandatoryPercentage(supplementPolicy())) { + return formatPercentage(srcPriceCts()); + } return SaleableTicketCategory.getFinalPriceToDisplay(getFinalPrice().add(getAppliedDiscount()), getVAT(), getVatStatus()).toPlainString(); } public boolean getSupportsDiscount() { - return getType() != AdditionalService.AdditionalServiceType.DONATION && isFixPrice() + return type() != AdditionalService.AdditionalServiceType.DONATION && fixPrice() && promoCodeDiscount != null && promoCodeDiscount.getCodeType() == PromoCodeDiscount.CodeType.DISCOUNT; } @@ -111,26 +116,23 @@ public String getDiscountedPrice() { } public boolean getVatIncluded() { - switch (getVatType()) { - case INHERITED: - return event.isVatIncluded(); - case CUSTOM_INCLUDED: - return true; - default: - return false; - } + return switch (vatType()) { + case INHERITED -> event.isVatIncluded(); + case CUSTOM_INCLUDED -> true; + default -> false; + }; } public BigDecimal getVatPercentage() { - AdditionalService.VatType vatType = getVatType(); + AdditionalService.VatType vatType = vatType(); if(vatType == AdditionalService.VatType.INHERITED) { return event.getVat(); } - return Optional.ofNullable(additionalService.getVat()).orElse(BigDecimal.ZERO); + return Optional.ofNullable(additionalService.vat()).orElse(BigDecimal.ZERO); } public boolean getVatApplies() { - return getVatType() != AdditionalService.VatType.NONE; + return vatType() != AdditionalService.VatType.NONE; } @@ -139,7 +141,7 @@ public String getCurrency() { } public boolean isSaleable() { - return !isExpired() && additionalService.getAvailableItems() > 0; + return !isExpired() && additionalService.availableItems() > 0; } private interface Exclusions { diff --git a/src/main/java/alfio/manager/AdditionalServiceManager.java b/src/main/java/alfio/manager/AdditionalServiceManager.java index c9cfaeb575..ff0ccfc745 100644 --- a/src/main/java/alfio/manager/AdditionalServiceManager.java +++ b/src/main/java/alfio/manager/AdditionalServiceManager.java @@ -18,6 +18,7 @@ import alfio.controller.form.AdditionalServiceLinkForm; import alfio.manager.support.reservation.NotEnoughItemsException; +import alfio.manager.support.reservation.ReservationCostCalculator; import alfio.model.*; import alfio.model.decorator.AdditionalServicePriceContainer; import alfio.model.modification.ASReservationWithOptionalCodeModification; @@ -46,9 +47,11 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import static alfio.model.AdditionalService.SupplementPolicy.MANDATORY_ONE_FOR_TICKET; -import static alfio.util.MonetaryUtil.unitToCents; +import static alfio.model.AdditionalService.SupplementPolicy.*; +import static alfio.util.MonetaryUtil.*; +import static java.math.RoundingMode.HALF_UP; import static java.util.Objects.requireNonNullElse; +import static java.util.stream.Collectors.groupingBy; @Component @AllArgsConstructor @@ -62,6 +65,7 @@ public class AdditionalServiceManager { private final NamedParameterJdbcTemplate jdbcTemplate; private final TicketRepository ticketRepository; private final PurchaseContextFieldRepository purchaseContextFieldRepository; + private final ReservationCostCalculator reservationCostCalculator; public List loadAllForEvent(int eventId) { @@ -127,7 +131,7 @@ public List exportItemsForEvent(AdditionalService.A public EventModification.AdditionalService insertAdditionalService(Event event, EventModification.AdditionalService additionalService) { int eventId = event.getId(); AffectedRowCountAndKey result = additionalServiceRepository.insert(eventId, - Optional.ofNullable(additionalService.getPrice()).map(p -> MonetaryUtil.unitToCents(p, event.getCurrency())).orElse(0), + evaluateAdditionalServicePriceCts(additionalService, event.getCurrency()), additionalService.isFixPrice(), additionalService.getOrdinal(), additionalService.getAvailableQuantity(), @@ -137,7 +141,9 @@ public EventModification.AdditionalService insertAdditionalService(Event event, additionalService.getVat(), additionalService.getVatType(), additionalService.getType(), - additionalService.getSupplementPolicy()); + additionalService.getSupplementPolicy(), + additionalService.getMinPrice() != null ? MonetaryUtil.unitToCents(additionalService.getMinPrice(), event.getCurrency()) : null, + additionalService.getMaxPrice() != null ? MonetaryUtil.unitToCents(additionalService.getMaxPrice(), event.getCurrency()) : null); Validate.isTrue(result.getAffectedRowCount() == 1, "too many records updated"); int id = result.getKey(); Stream.concat(additionalService.getTitle().stream(), additionalService.getDescription().stream()). @@ -159,7 +165,7 @@ void createAllAdditionalServices(Event event, List { AffectedRowCountAndKey service = additionalServiceRepository.insert(eventId, - Optional.ofNullable(as.getPrice()).map(p -> MonetaryUtil.unitToCents(p, currencyCode)).orElse(0), + evaluateAdditionalServicePriceCts(as, currencyCode), as.isFixPrice(), as.getOrdinal(), as.getAvailableQuantity(), @@ -169,7 +175,9 @@ void createAllAdditionalServices(Event event, List 0) { preGenerateItems(service.getKey(), event, as); } @@ -179,6 +187,17 @@ void createAllAdditionalServices(Event event, List(); - if (as.getAvailableQuantity() > 0) { + if (as.availableQuantity() > 0) { queryTemplate = additionalServiceItemRepository.batchUpdate(); - var ids = additionalServiceItemRepository.lockExistingItems(as.getId(), quantity); + var ids = additionalServiceItemRepository.lockExistingItems(as.id(), quantity); if (ids.size() != quantity) { throw new NotEnoughItemsException(); } for (int i = 0; i < quantity; i++) { batchReserveParameters.add(new MapSqlParameterSource("uuid", UUID.randomUUID().toString()) .addValue("ticketsReservationUuid", reservationId) - .addValue("additionalServiceId", as.getId()) + .addValue("additionalServiceId", as.id()) .addValue("status", AdditionalServiceItem.AdditionalServiceItemStatus.PENDING.name()) .addValue("id", ids.get(i)) .addValue("srcPriceCts", pc.getSrcPriceCts()) @@ -249,7 +268,7 @@ void bookAdditionalServiceItems(int quantity, queryTemplate = additionalServiceItemRepository.batchInsert(); for (int i = 0; i < quantity; i++) { batchReserveParameters.add(buildInsertItemParameterSource( - as.getId(), + as.id(), reservationId, AdditionalServiceItem.AdditionalServiceItemStatus.PENDING, event, @@ -383,13 +402,13 @@ public void bookAdditionalServicesForReservation(Event event, // apply valid additional service with supplement policy mandatory one for ticket var additionalServicesForEvent = loadAllForEvent(event.getId()); - var automatic = additionalServicesForEvent.stream().filter(as -> as.getSupplementPolicy() == MANDATORY_ONE_FOR_TICKET && as.getSaleable()) + var automatic = additionalServicesForEvent.stream().filter(as -> as.supplementPolicy().isMandatory() && as.getSaleable()) .map(as -> { AdditionalServiceReservationModification asrm = new AdditionalServiceReservationModification(); - asrm.setAdditionalServiceId(as.getId()); - asrm.setQuantity(ticketCount); + asrm.setAdditionalServiceId(as.id()); + asrm.setQuantity(as.supplementPolicy() == MANDATORY_ONE_FOR_TICKET ? ticketCount : 1); return new ASReservationWithOptionalCodeModification(asrm, Optional.empty()); - }).collect(Collectors.toList()); + }).toList(); if (automatic.isEmpty() && additionalServices.isEmpty()) { // skip additional queries @@ -412,7 +431,7 @@ public void persistFieldsForAdditionalItems(int eventId, var fields = purchaseContextFieldRepository.findAdditionalFieldsForEvent(eventId) .stream() .filter(c -> c.getContext() == PurchaseContextFieldConfiguration.Context.ADDITIONAL_SERVICE) - .collect(Collectors.toList()); + .toList(); var sources = additionalServices.entrySet().stream() .flatMap(entry -> entry.getValue().stream().flatMap(form -> form.getAdditional().entrySet().stream() @@ -436,37 +455,54 @@ private void reserveAdditionalServicesForReservation(Event event, PromoCodeDiscount discount, List additionalServicesForEvent, List ticketIds) { - additionalServiceReservationList.forEach(additionalServiceReservation -> { - if (additionalServiceReservation.getAdditionalServiceId() == null) { - return; - } - var optionalAs = additionalServicesForEvent.stream() - .filter(as -> as.getId() == additionalServiceReservation.getAdditionalServiceId()) - .findFirst(); - if (optionalAs.isEmpty()) { - return; - } - var as = optionalAs.get(); - if (additionalServiceReservation.getQuantity() > 0 && (as.isFixPrice() || requireNonNullElse(additionalServiceReservation.getAmount(), BigDecimal.ZERO).compareTo(BigDecimal.ZERO) > 0)) { + + + var allAdditionalItems = additionalServiceReservationList.stream() + .filter(ar -> ar.getAdditionalServiceId() != null) + .map(requested -> { + var optionalAs = additionalServicesForEvent.stream() + .filter(as -> as.id() == requested.getAdditionalServiceId() && as.supplementPolicy() != null) + .findFirst(); + return new MappedRequestedService(requested, optionalAs.orElse(null)); + }) + .filter(o -> Objects.nonNull(o.additionalService)) + .collect(groupingBy(o -> o.additionalService.supplementPolicy())); + + // first handle MANDATORY_PERCENTAGE_FOR_TICKET, if any. + // this way only ticket costs will be included in the percentage calculation + handleMandatoryPercentage(MANDATORY_PERCENTAGE_FOR_TICKET, event, reservationId, discount, allAdditionalItems); + + // then apply all non-mandatory (i.e. user-selected) + var nonMandatoryPolicies = AdditionalService.SupplementPolicy.userSelected(); + nonMandatoryPolicies.stream() + .filter(allAdditionalItems::containsKey) + .flatMap(p -> allAdditionalItems.get(p).stream().filter(as -> as.requested.getQuantity() > 0 && (as.additionalService.fixPrice() || requireNonNullElse(as.requested.getAmount(), BigDecimal.ZERO).compareTo(BigDecimal.ZERO) > 0))) + .forEach(mapped -> { + var as = mapped.additionalService; + var additionalServiceReservation = mapped.requested; bookAdditionalServiceItems(additionalServiceReservation.getQuantity(), additionalServiceReservation.getAmount(), as, event, discount, reservationId); - } + }); + + // as last step, we apply all remaining mandatory + handleMandatoryPercentage(MANDATORY_PERCENTAGE_RESERVATION, event, reservationId, discount, allAdditionalItems); + + allAdditionalItems.getOrDefault(MANDATORY_ONE_FOR_TICKET, List.of()).forEach(mrs -> { + BigDecimal amount = mrs.requested.getAmount(); + bookAdditionalServiceItems(mrs.requested.getQuantity(), amount, mrs.additionalService, event, discount, reservationId); }); // link additional services to tickets var bookedItems = additionalServiceItemRepository.findByReservationUuid(event.getId(), reservationId); //we skip donation as they don't have a supplement policy - var byPolicy = additionalServicesForEvent.stream() - .filter(as -> as.getSupplementPolicy() != null && additionalServiceReservationList.stream().anyMatch(findAdditionalServiceRequest(as))) - .collect(Collectors.groupingBy(AdditionalService::getSupplementPolicy)); - var parameterSources = byPolicy.entrySet().stream() + var parameterSources = allAdditionalItems.entrySet().stream() .flatMap(entry -> { var values = entry.getValue(); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Processing {} items with policy {}", values.size(), entry.getKey()); } return values.stream() - .flatMap(m -> linkWithEveryTicket(reservationId, additionalServiceReservationList, bookedItems, ticketIds, m)); + .flatMap(m -> linkWithEveryTicket(reservationId, additionalServiceReservationList, bookedItems, ticketIds, m.additionalService)); }).toArray(MapSqlParameterSource[]::new); var results = jdbcTemplate.batchUpdate(additionalServiceItemRepository.batchLinkToTicket(), parameterSources); Validate.isTrue(Arrays.stream(results).allMatch(i -> i == 1)); @@ -474,13 +510,46 @@ private void reserveAdditionalServicesForReservation(Event event, // we attach all those without policy to the first ticket (donations) var firstTicketId = ticketIds.stream().findFirst().map(List::of).orElseThrow(); var noPoliciesParameterSources = additionalServicesForEvent.stream() - .filter(as -> as.getSupplementPolicy() == null && additionalServiceReservationList.stream().anyMatch(findAdditionalServiceRequest(as))) + .filter(as -> as.supplementPolicy() == null && additionalServiceReservationList.stream().anyMatch(findAdditionalServiceRequest(as))) .flatMap(as -> linkWithEveryTicket(reservationId, additionalServiceReservationList, bookedItems, firstTicketId, as)) .toArray(MapSqlParameterSource[]::new); var noPolicyResults = jdbcTemplate.batchUpdate(additionalServiceItemRepository.batchLinkToTicket(), noPoliciesParameterSources); Validate.isTrue(Arrays.stream(noPolicyResults).allMatch(i -> i == 1)); } + private void handleMandatoryPercentage(AdditionalService.SupplementPolicy supplementPolicy, + Event event, + String reservationId, + PromoCodeDiscount discount, + Map> allMapped) { + if (allMapped.containsKey(supplementPolicy)) { + final TotalPrice reservationPrice = reservationCostCalculator.totalReservationCostWithVAT(reservationId).getKey(); + allMapped.get(supplementPolicy).forEach(mrs -> { + int basePrice = reservationPrice.getPriceWithVAT(); + var vatStatus = event.getVatStatus(); + if (PriceContainer.VatStatus.isVatNotIncluded(vatStatus)) { + basePrice -= reservationPrice.getVAT(); + } + var percentage = new BigDecimal(String.valueOf(mrs.additionalService.srcPriceCts())).divide(HUNDRED, HALF_UP); + int amountCts = adjustUsingMinMaxPrice(calcPercentage(basePrice, percentage, BigDecimal::intValueExact), mrs.additionalService); + BigDecimal amount = centsToUnit(amountCts, reservationPrice.getCurrencyCode()); + bookAdditionalServiceItems(mrs.requested.getQuantity(), amount, mrs.additionalService, event, discount, reservationId); + }); + } + } + + private static int adjustUsingMinMaxPrice(int amountCts, AdditionalService additionalService) { + if (additionalService.getMinPrice() != null && unitToCents(additionalService.getMinPrice(), additionalService.currencyCode()) > amountCts) { + // if calculated price is below minimum, we return the minimum price + return unitToCents(additionalService.getMinPrice(), additionalService.currencyCode()); + } + if (additionalService.getMaxPrice() != null && unitToCents(additionalService.getMaxPrice(), additionalService.currencyCode()) < amountCts) { + // if calculated price is over maximum, we return the maximum price + return unitToCents(additionalService.getMaxPrice(), additionalService.currencyCode()); + } + return amountCts; + } + private static Stream linkWithEveryTicket(String reservationId, List additionalServiceReservationList, List bookedItems, List ticketIds, AdditionalService m) { var additionalServiceRequest = additionalServiceReservationList.stream() .filter(findAdditionalServiceRequest(m)) @@ -493,6 +562,9 @@ private static Stream linkWithEveryTicket(String reservat } private static Predicate findAdditionalServiceRequest(AdditionalService as) { - return asr -> as.getId() == asr.getAdditionalServiceId(); + return asr -> as.id() == asr.getAdditionalServiceId(); + } + + record MappedRequestedService(ASReservationWithOptionalCodeModification requested, AdditionalService additionalService) { } } diff --git a/src/main/java/alfio/manager/EventNameManager.java b/src/main/java/alfio/manager/EventNameManager.java index c6275e20ef..c4d0f9eadb 100644 --- a/src/main/java/alfio/manager/EventNameManager.java +++ b/src/main/java/alfio/manager/EventNameManager.java @@ -45,7 +45,7 @@ public class EventNameManager { private static final RandomStringGenerator RANDOM_STRING_GENERATOR = new RandomStringGenerator.Builder() .withinRange(new char[] {'a', 'z'}, new char[] {'A', 'Z'} , new char[] { '0', '9'}) .usingRandom(RANDOM::nextInt) - .build(); + .get(); private final EventAdminRepository eventAdminRepository; /** diff --git a/src/main/java/alfio/manager/PurchaseContextFieldManager.java b/src/main/java/alfio/manager/PurchaseContextFieldManager.java index 6b0e01f5d7..08f8ebd5e4 100644 --- a/src/main/java/alfio/manager/PurchaseContextFieldManager.java +++ b/src/main/java/alfio/manager/PurchaseContextFieldManager.java @@ -187,8 +187,11 @@ private Integer findAdditionalService(Event event, EventModification.AdditionalS Optional.ofNullable(as.getPrice()).map(p -> MonetaryUtil.unitToCents(p, currencyCode)).orElse(0), as.getType(), as.getSupplementPolicy(), - currencyCode, null).getChecksum(); - return additionalServiceRepository.loadAllForEvent(eventId).stream().filter(as1 -> as1.getChecksum().equals(checksum)).findFirst().map(AdditionalService::getId).orElse(null); + currencyCode, + null, + as.getMinPrice() != null ? MonetaryUtil.unitToCents(as.getMinPrice(), currencyCode) : null, + as.getMaxPrice() != null ? MonetaryUtil.unitToCents(as.getMaxPrice(), currencyCode) : null).getChecksum(); + return additionalServiceRepository.loadAllForEvent(eventId).stream().filter(as1 -> as1.getChecksum().equals(checksum)).findFirst().map(AdditionalService::id).orElse(null); } public void updateFieldDescriptions(Map descriptions, int organizationId) { diff --git a/src/main/java/alfio/manager/support/reservation/OrderSummaryGenerator.java b/src/main/java/alfio/manager/support/reservation/OrderSummaryGenerator.java index 8819864590..068a2fcaf5 100644 --- a/src/main/java/alfio/manager/support/reservation/OrderSummaryGenerator.java +++ b/src/main/java/alfio/manager/support/reservation/OrderSummaryGenerator.java @@ -146,7 +146,7 @@ List extractSummary(PriceContainer.VatStatus reservationVatStatus, List summary = new ArrayList<>(); var currencyCode = reservationCost.getCurrencyCode(); List tickets = ticketsToInclude.stream() - .map(t -> TicketPriceContainer.from(t, reservationVatStatus, purchaseContext.getVat(), purchaseContext.getVatStatus(), promoCodeDiscount)).collect(toList()); + .map(t -> TicketPriceContainer.from(t, reservationVatStatus, purchaseContext.getVat(), purchaseContext.getVatStatus(), promoCodeDiscount)).toList(); purchaseContext.event().ifPresent(event -> { boolean multipleTaxRates = tickets.stream().map(TicketPriceContainer::getVatStatus).collect(Collectors.toSet()).size() > 1; var ticketsByCategory = tickets.stream() @@ -157,7 +157,7 @@ List extractSummary(PriceContainer.VatStatus reservationVatStatus, .entrySet() .stream() .sorted(Comparator.comparing((Map.Entry> e) -> e.getValue().get(0).getVatStatus()).reversed()) - .collect(Collectors.toList()); + .toList(); } else { sorted = new ArrayList<>(ticketsByCategory.entrySet()); } @@ -198,7 +198,7 @@ List extractSummary(PriceContainer.VatStatus reservationVatStatus, summary.addAll(additionalServicesToInclude .map(entry -> { String language = locale.getLanguage(); - AdditionalServiceText title = additionalServiceTextRepository.findBestMatchByLocaleAndType(entry.getKey().getId(), language, AdditionalServiceText.TextType.TITLE); + AdditionalServiceText title = additionalServiceTextRepository.findBestMatchByLocaleAndType(entry.getKey().id(), language, AdditionalServiceText.TextType.TITLE); if(!title.getLocale().equals(language) || title.getId() == -1) { log.debug("additional service {}: title not found for locale {}", title.getAdditionalServiceId(), language); } @@ -206,8 +206,14 @@ List extractSummary(PriceContainer.VatStatus reservationVatStatus, AdditionalServiceItemPriceContainer first = prices.get(0); final int subtotal = prices.stream().mapToInt(AdditionalServiceItemPriceContainer::getSrcPriceCts).sum(); final int subtotalBeforeVat = SummaryPriceContainer.getSummaryPriceBeforeVatCts(prices); - return new SummaryRow(title.getValue(), formatCents(first.getSrcPriceCts(), currencyCode), formatCents(SummaryPriceContainer.getSummaryPriceBeforeVatCts(singletonList(first)), currencyCode), prices.size(), formatCents(subtotal, currencyCode), formatCents(subtotalBeforeVat, currencyCode), subtotal, SummaryRow.SummaryType.ADDITIONAL_SERVICE, null, first.getVatStatus()); - }).collect(toList())); + Function formatterFunction; + if (AdditionalService.SupplementPolicy.isMandatoryPercentage(entry.getKey().supplementPolicy())) { + formatterFunction = cts -> MonetaryUtil.formatPercentage(cts) + "%"; + } else { + formatterFunction = srcPriceCts -> formatCents(srcPriceCts, currencyCode); + } + return new SummaryRow(title.getValue(), formatterFunction.apply(first.getSrcPriceCts()), formatterFunction.apply(SummaryPriceContainer.getSummaryPriceBeforeVatCts(singletonList(first))), prices.size(), formatCents(subtotal, currencyCode), formatCents(subtotalBeforeVat, currencyCode), subtotal, SummaryRow.SummaryType.ADDITIONAL_SERVICE, null, first.getVatStatus()); + }).toList()); Optional.ofNullable(promoCodeDiscount).ifPresent(promo -> { String formattedSingleAmount = "-" + (PromoCodeDiscount.DiscountType.isFixedAmount(promo.getDiscountType()) ? formatCents(promo.getDiscountAmount(), currencyCode) : (promo.getDiscountAmount()+"%")); @@ -242,7 +248,7 @@ List extractSummary(PriceContainer.VatStatus reservationVatStatus, subscriptionRepository.findOne(subscription.getSubscriptionDescriptorId(), subscription.getOrganizationId()).ifPresent(subscriptionDescriptor -> { log.trace("found subscriptionDescriptor with ID {}", subscriptionDescriptor.getId()); // find tickets with subscription applied - var ticketsSubscription = tickets.stream().filter(t -> Objects.equals(subscription.getId(), t.getSubscriptionId())).collect(toList()); + var ticketsSubscription = tickets.stream().filter(t -> Objects.equals(subscription.getId(), t.getSubscriptionId())).toList(); final int ticketPriceCts = ticketsSubscription.stream().mapToInt(TicketPriceContainer::getSummarySrcPriceCts).sum(); final int priceBeforeVat = SummaryPriceContainer.getSummaryPriceBeforeVatCts(ticketsSubscription); summary.add(new SummaryRow(subscriptionDescriptor.getLocalizedTitle(locale), @@ -289,7 +295,7 @@ private String formatPromoCode(PromoCodeDiscount promoCodeDiscount, List return messageSourceManager.getMessageSourceFor(purchaseContext).getMessage("reservation.dynamic.discount.description", null, locale); //we don't expose the internal promo code } - List filteredTickets = tickets.stream().filter(ticket -> promoCodeDiscount.getCategories().contains(ticket.getCategoryId())).collect(toList()); + List filteredTickets = tickets.stream().filter(ticket -> promoCodeDiscount.getCategories().contains(ticket.getCategoryId())).toList(); if (promoCodeDiscount.getCategories().isEmpty() || filteredTickets.isEmpty()) { return promoCodeDiscount.getPromoCode(); diff --git a/src/main/java/alfio/manager/system/ReservationPriceCalculator.java b/src/main/java/alfio/manager/system/ReservationPriceCalculator.java index f782136553..a1dfd80056 100644 --- a/src/main/java/alfio/manager/system/ReservationPriceCalculator.java +++ b/src/main/java/alfio/manager/system/ReservationPriceCalculator.java @@ -98,7 +98,7 @@ public BigDecimal getTaxablePrice() { .map(t -> TicketPriceContainer.from(t, reservation.getVatStatus(), getVatPercentageOrZero(), purchaseContext.getVatStatus(), discount).getTaxablePrice()) .reduce(BigDecimal.ZERO, BigDecimal::add); var additionalServiceTaxablePrice = additionalServiceItems.stream() - .map(asi -> AdditionalServiceItemPriceContainer.from(asi, additionalServices.stream().filter(as -> as.getId() == asi.getAdditionalServiceId()).findFirst().orElseThrow(), purchaseContext, discount).getTaxablePrice()) + .map(asi -> AdditionalServiceItemPriceContainer.from(asi, additionalServices.stream().filter(as -> as.id() == asi.getAdditionalServiceId()).findFirst().orElseThrow(), purchaseContext, discount).getTaxablePrice()) .reduce(BigDecimal.ZERO, BigDecimal::add); var subscriptionsPrice = subscriptions.stream().map(s -> SubscriptionPriceContainer.from(s, purchaseContext, discount).getTaxablePrice()) .reduce(BigDecimal.ZERO, BigDecimal::add); diff --git a/src/main/java/alfio/model/AdditionalService.java b/src/main/java/alfio/model/AdditionalService.java index fe81b33eaa..59230b69da 100644 --- a/src/main/java/alfio/model/AdditionalService.java +++ b/src/main/java/alfio/model/AdditionalService.java @@ -17,8 +17,8 @@ package alfio.model; import alfio.util.ClockProvider; +import alfio.util.MonetaryUtil; import ch.digitalfondue.npjt.ConstructorAnnotationRowMapper.Column; -import lombok.Getter; import org.springframework.security.crypto.codec.Hex; import java.math.BigDecimal; @@ -28,10 +28,29 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; +import java.util.Arrays; import java.util.Optional; - -@Getter -public class AdditionalService { +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public record AdditionalService(@Column("id") int id, + @Column("event_id_fk") int eventId, + @Column("fix_price") boolean fixPrice, + @Column("ordinal") int ordinal, + @Column("available_qty") int availableQuantity, + @Column("max_qty_per_order") int maxQtyPerOrder, + @Column("inception_ts") ZonedDateTime utcInception, + @Column("expiration_ts") ZonedDateTime utcExpiration, + @Column("vat") BigDecimal vat, + @Column("vat_type") VatType vatType, + @Column("src_price_cts") Integer srcPriceCts, + @Column("service_type") AdditionalServiceType type, + @Column("supplement_policy") SupplementPolicy supplementPolicy, + @Column("currency_code") String currencyCode, + @Column("available_count") Integer availableItems, + @Column("price_min_cts") Integer minPriceCts, + @Column("price_max_cts") Integer maxPriceCts) { public enum VatType { @@ -46,73 +65,56 @@ public enum AdditionalServiceType { } public enum SupplementPolicy { - MANDATORY_ONE_FOR_TICKET, + MANDATORY_ONE_FOR_TICKET(true), + /** + * Will be calculated from the total reservation price, excluding any other mandatory fee + */ + MANDATORY_PERCENTAGE_RESERVATION(true), + /** + * Will be calculated from the total price of the tickets, excluding any additional items + */ + MANDATORY_PERCENTAGE_FOR_TICKET(true), OPTIONAL_UNLIMITED_AMOUNT, OPTIONAL_MAX_AMOUNT_PER_TICKET { @Override public boolean isValid(int quantity, AdditionalService as, int ticketsCount) { - return quantity <= ticketsCount * as.getMaxQtyPerOrder(); + return quantity <= ticketsCount * as.maxQtyPerOrder(); } }, OPTIONAL_MAX_AMOUNT_PER_RESERVATION { @Override public boolean isValid(int quantity, AdditionalService as, int selectionCount) { - return quantity <= as.getMaxQtyPerOrder(); + return quantity <= as.maxQtyPerOrder(); } }; + private final boolean mandatory; + + SupplementPolicy() { + this(false); + } + + SupplementPolicy(boolean mandatory) { + this.mandatory = mandatory; + } + + public boolean isValid(int quantity, AdditionalService as, int selectionCount) { return true; } - } - private final int id; - private final int eventId; - private final boolean fixPrice; - private final int ordinal; - private final int availableQuantity; - private final int maxQtyPerOrder; - private final ZonedDateTime utcInception; - private final ZonedDateTime utcExpiration; - private final BigDecimal vat; - private final VatType vatType; - private final AdditionalServiceType type; - private final SupplementPolicy supplementPolicy; - - private final Integer srcPriceCts; - private final String currencyCode; - private final Integer availableItems; - - public AdditionalService(@Column("id") int id, - @Column("event_id_fk") int eventId, - @Column("fix_price") boolean fixPrice, - @Column("ordinal") int ordinal, - @Column("available_qty") int availableQuantity, - @Column("max_qty_per_order") int maxQtyPerOrder, - @Column("inception_ts") ZonedDateTime utcInception, - @Column("expiration_ts") ZonedDateTime utcExpiration, - @Column("vat") BigDecimal vat, - @Column("vat_type") VatType vatType, - @Column("src_price_cts") Integer srcPriceCts, - @Column("service_type") AdditionalServiceType type, - @Column("supplement_policy") SupplementPolicy supplementPolicy, - @Column("currency_code") String currencyCode, - @Column("available_count") Integer availableItems) { - this.id = id; - this.eventId = eventId; - this.fixPrice = fixPrice; - this.ordinal = ordinal; - this.availableQuantity = availableQuantity; - this.maxQtyPerOrder = maxQtyPerOrder; - this.utcInception = utcInception; - this.utcExpiration = utcExpiration; - this.vat = vat; - this.vatType = vatType; - this.srcPriceCts = srcPriceCts; - this.type = type; - this.supplementPolicy = supplementPolicy; - this.currencyCode = currencyCode; - this.availableItems = availableItems; + public boolean isMandatory() { + return mandatory; + } + + public static Set userSelected() { + return Arrays.stream(values()).filter(Predicate.not(SupplementPolicy::isMandatory)).collect(Collectors.toSet()); + } + + public static boolean isMandatoryPercentage(SupplementPolicy policy) { + return policy == SupplementPolicy.MANDATORY_PERCENTAGE_RESERVATION + || policy == SupplementPolicy.MANDATORY_PERCENTAGE_FOR_TICKET; + } } public ZonedDateTime getInception(ZoneId zoneId) { @@ -125,7 +127,21 @@ public ZonedDateTime getExpiration(ZoneId zoneId) { public boolean getSaleable() { ZonedDateTime now = ZonedDateTime.now(ClockProvider.clock()); - return getUtcInception().isBefore(now) && getUtcExpiration().isAfter(now); + return utcInception().isBefore(now) && utcExpiration().isAfter(now); + } + + public BigDecimal getMinPrice() { + if (minPriceCts != null) { + return MonetaryUtil.centsToUnit(minPriceCts, currencyCode); + } + return null; + } + + public BigDecimal getMaxPrice() { + if (maxPriceCts != null) { + return MonetaryUtil.centsToUnit(maxPriceCts, currencyCode); + } + return null; } public String getChecksum() { @@ -150,17 +166,11 @@ public String getChecksum() { } public static PriceContainer.VatStatus getVatStatus(VatType vatType, PriceContainer.VatStatus eventVatStatus) { - switch (vatType) { - case INHERITED: - return eventVatStatus; - case NONE: - return PriceContainer.VatStatus.NONE; - case CUSTOM_EXCLUDED: - return PriceContainer.VatStatus.NOT_INCLUDED; - case CUSTOM_INCLUDED: - return PriceContainer.VatStatus.INCLUDED; - default: - return PriceContainer.VatStatus.NOT_INCLUDED; - } + return switch (vatType) { + case INHERITED -> eventVatStatus; + case NONE -> PriceContainer.VatStatus.NONE; + case CUSTOM_INCLUDED -> PriceContainer.VatStatus.INCLUDED; + default -> PriceContainer.VatStatus.NOT_INCLUDED; + }; } } diff --git a/src/main/java/alfio/model/PriceContainer.java b/src/main/java/alfio/model/PriceContainer.java index a443a94b9b..a2b65cd01c 100644 --- a/src/main/java/alfio/model/PriceContainer.java +++ b/src/main/java/alfio/model/PriceContainer.java @@ -73,6 +73,15 @@ public static boolean isVatIncluded(VatStatus vatStatus) { return vatStatus == INCLUDED || vatStatus == INCLUDED_EXEMPT || vatStatus == CUSTOM_INCLUDED_EXEMPT; } + /** + * Checks if tax is explicitly NOT INCLUDED + * @param vatStatus tax application + * @return true if not included + */ + public static boolean isVatNotIncluded(VatStatus vatStatus) { + return vatStatus == NOT_INCLUDED || vatStatus == NOT_INCLUDED_EXEMPT || vatStatus == CUSTOM_NOT_INCLUDED_EXEMPT; + } + public static VatStatus forceExempt(VatStatus original) { if (isVatIncluded(original)) { return INCLUDED_EXEMPT; diff --git a/src/main/java/alfio/model/decorator/AdditionalServiceItemPriceContainer.java b/src/main/java/alfio/model/decorator/AdditionalServiceItemPriceContainer.java index 929ec51114..509a17c0ea 100644 --- a/src/main/java/alfio/model/decorator/AdditionalServiceItemPriceContainer.java +++ b/src/main/java/alfio/model/decorator/AdditionalServiceItemPriceContainer.java @@ -52,7 +52,7 @@ public String getCurrencyCode() { @Override public Optional getOptionalVatPercentage() { - if(additionalService.getVatType() == AdditionalService.VatType.INHERITED) { + if(additionalService.vatType() == AdditionalService.VatType.INHERITED) { return Optional.ofNullable(eventVatPercentage); } else { return Optional.empty(); @@ -61,7 +61,7 @@ public Optional getOptionalVatPercentage() { @Override public VatStatus getVatStatus() { - if(additionalService.getVatType() == AdditionalService.VatType.INHERITED) { + if(additionalService.vatType() == AdditionalService.VatType.INHERITED) { return eventVatStatus; } else { return VatStatus.NONE; @@ -77,7 +77,7 @@ public BigDecimal getTaxablePrice() { } public static AdditionalServiceItemPriceContainer from(AdditionalServiceItem item, AdditionalService additionalService, PurchaseContext purchaseContext, PromoCodeDiscount discount) { - var discountToApply = isDiscountCompatible(discount) && additionalService.getType() != AdditionalService.AdditionalServiceType.DONATION ? discount : null; + var discountToApply = isDiscountCompatible(discount) && additionalService.type() != AdditionalService.AdditionalServiceType.DONATION ? discount : null; return new AdditionalServiceItemPriceContainer(item, additionalService, purchaseContext.getCurrency(), discountToApply, purchaseContext.getVatStatus(), purchaseContext.getVat()); } diff --git a/src/main/java/alfio/model/decorator/AdditionalServicePriceContainer.java b/src/main/java/alfio/model/decorator/AdditionalServicePriceContainer.java index 2f6d31902a..6867a88b66 100644 --- a/src/main/java/alfio/model/decorator/AdditionalServicePriceContainer.java +++ b/src/main/java/alfio/model/decorator/AdditionalServicePriceContainer.java @@ -39,15 +39,15 @@ public class AdditionalServicePriceContainer implements PriceContainer { @Override public int getSrcPriceCts() { - if(additionalService.isFixPrice()) { - return additionalService.getSrcPriceCts(); + if(additionalService.fixPrice()) { + return additionalService.srcPriceCts(); } return Optional.ofNullable(customAmount).map(a -> MonetaryUtil.unitToCents(a, currencyCode)).orElse(0); } @Override public Optional getDiscount() { - return Optional.ofNullable(promoCodeDiscount).filter(d -> additionalService.getType() != AdditionalService.AdditionalServiceType.DONATION); + return Optional.ofNullable(promoCodeDiscount).filter(d -> additionalService.type() != AdditionalService.AdditionalServiceType.DONATION); } @Override @@ -57,7 +57,7 @@ public String getCurrencyCode() { @Override public Optional getOptionalVatPercentage() { - if(additionalService.getVatType() == AdditionalService.VatType.INHERITED) { + if(additionalService.vatType() == AdditionalService.VatType.INHERITED) { return Optional.ofNullable(vatPercentage); } else { return Optional.empty(); @@ -66,7 +66,7 @@ public Optional getOptionalVatPercentage() { @Override public VatStatus getVatStatus() { - if(additionalService.getVatType() == AdditionalService.VatType.INHERITED) { + if(additionalService.vatType() == AdditionalService.VatType.INHERITED) { return vatStatus; } else { return VatStatus.NONE; diff --git a/src/main/java/alfio/model/modification/AdditionalServiceReservationModification.java b/src/main/java/alfio/model/modification/AdditionalServiceReservationModification.java index 18325a8a92..0498f269d9 100644 --- a/src/main/java/alfio/model/modification/AdditionalServiceReservationModification.java +++ b/src/main/java/alfio/model/modification/AdditionalServiceReservationModification.java @@ -29,8 +29,8 @@ public class AdditionalServiceReservationModification implements Serializable { private Integer quantity = 1; public boolean isQuantityValid(AdditionalService as, int selectionCount) { - if(quantity != null && as.getSupplementPolicy() != null) { - return as.getSupplementPolicy().isValid(quantity, as, selectionCount); + if(quantity != null && as.supplementPolicy() != null) { + return as.supplementPolicy().isValid(quantity, as, selectionCount); } else { return true; } diff --git a/src/main/java/alfio/model/modification/EventModification.java b/src/main/java/alfio/model/modification/EventModification.java index ce2c944910..45ef37320b 100644 --- a/src/main/java/alfio/model/modification/EventModification.java +++ b/src/main/java/alfio/model/modification/EventModification.java @@ -264,6 +264,8 @@ public static class AdditionalService { private final String currency; private final alfio.model.AdditionalService.AdditionalServiceType type; private final alfio.model.AdditionalService.SupplementPolicy supplementPolicy; + private final BigDecimal minPrice; + private final BigDecimal maxPrice; @JsonCreator public AdditionalService(@JsonProperty("id") Integer id, @@ -280,8 +282,10 @@ public AdditionalService(@JsonProperty("id") Integer id, @JsonProperty("title") List title, @JsonProperty("description") List description, @JsonProperty("type")alfio.model.AdditionalService.AdditionalServiceType type, - @JsonProperty("supplementPolicy")alfio.model.AdditionalService.SupplementPolicy supplementPolicy) { - this(id, price, fixPrice, ordinal, availableQuantity, maxQtyPerOrder, inception, expiration, vat, vatType, additionalServiceFields, title, description, null, null, type, supplementPolicy); + @JsonProperty("supplementPolicy")alfio.model.AdditionalService.SupplementPolicy supplementPolicy, + @JsonProperty("minPrice") BigDecimal minPrice, + @JsonProperty("maxPrice") BigDecimal maxPrice) { + this(id, price, fixPrice, ordinal, availableQuantity, maxQtyPerOrder, inception, expiration, vat, vatType, additionalServiceFields, title, description, null, null, type, supplementPolicy, minPrice, maxPrice); } private AdditionalService(Integer id, @@ -300,7 +304,9 @@ private AdditionalService(Integer id, BigDecimal finalPrice, String currencyCode, alfio.model.AdditionalService.AdditionalServiceType type, - alfio.model.AdditionalService.SupplementPolicy supplementPolicy) { + alfio.model.AdditionalService.SupplementPolicy supplementPolicy, + BigDecimal minPrice, + BigDecimal maxPrice) { this.id = id; this.price = price; this.fixPrice = fixPrice; @@ -318,6 +324,8 @@ private AdditionalService(Integer id, this.currency = currencyCode; this.type = type; this.supplementPolicy = supplementPolicy; + this.minPrice = minPrice; + this.maxPrice = maxPrice; } public static Builder from(alfio.model.AdditionalService src) { @@ -360,9 +368,9 @@ public AdditionalService build() { Optional optionalPrice = Optional.ofNullable(this.priceContainer); BigDecimal finalPrice = optionalPrice.map(PriceContainer::getFinalPrice).orElse(BigDecimal.ZERO); String currencyCode = optionalPrice.map(PriceContainer::getCurrencyCode).orElse(""); - return new AdditionalService(src.getId(), Optional.ofNullable(src.getSrcPriceCts()).map(p -> MonetaryUtil.centsToUnit(p, src.getCurrencyCode())).orElse(BigDecimal.ZERO), - src.isFixPrice(), src.getOrdinal(), src.getAvailableQuantity(), src.getMaxQtyPerOrder(), DateTimeModification.fromZonedDateTime(src.getInception(zoneId)), - DateTimeModification.fromZonedDateTime(src.getExpiration(zoneId)), src.getVat(), src.getVatType(), additionalServiceFields, title, description, finalPrice, currencyCode, src.getType(), src.getSupplementPolicy()); + return new AdditionalService(src.id(), Optional.ofNullable(src.srcPriceCts()).map(p -> MonetaryUtil.centsToUnit(p, src.currencyCode())).orElse(BigDecimal.ZERO), + src.fixPrice(), src.ordinal(), src.availableQuantity(), src.maxQtyPerOrder(), DateTimeModification.fromZonedDateTime(src.getInception(zoneId)), + DateTimeModification.fromZonedDateTime(src.getExpiration(zoneId)), src.vat(), src.vatType(), additionalServiceFields, title, description, finalPrice, currencyCode, src.type(), src.supplementPolicy(), src.getMinPrice(), src.getMaxPrice()); } } diff --git a/src/main/java/alfio/repository/AdditionalServiceItemRepository.java b/src/main/java/alfio/repository/AdditionalServiceItemRepository.java index 18f6372c80..a6275cee3e 100644 --- a/src/main/java/alfio/repository/AdditionalServiceItemRepository.java +++ b/src/main/java/alfio/repository/AdditionalServiceItemRepository.java @@ -35,7 +35,7 @@ select asd.value as as_name, ads.id as_id, count(ads.id) as qty from additional_ join additional_service_description asd on ads.id = asd.additional_service_id_fk\ where ai.event_id_fk = :eventId and ai.status in ('ACQUIRED', 'CHECKED_IN', 'TO_BE_PAID')\ and ads.service_type <> 'DONATION'\ - and ads.supplement_policy <> 'MANDATORY_ONE_FOR_TICKET'\ + and ads.supplement_policy not in ('MANDATORY_ONE_FOR_TICKET', 'MANDATORY_PERCENTAGE_RESERVATION', 'MANDATORY_PERCENTAGE_FOR_TICKET')\ and asd.locale = :language\ and asd.type = 'TITLE'\ and ai.tickets_reservation_uuid = :reservationId\ diff --git a/src/main/java/alfio/repository/AdditionalServiceRepository.java b/src/main/java/alfio/repository/AdditionalServiceRepository.java index 11406ed6b4..12bcfb1bff 100644 --- a/src/main/java/alfio/repository/AdditionalServiceRepository.java +++ b/src/main/java/alfio/repository/AdditionalServiceRepository.java @@ -59,8 +59,8 @@ default Map> getCount(int eve int delete(@Bind("id") int id, @Bind("eventId") int eventId); @Query(""" - insert into additional_service (event_id_fk, fix_price, ordinal, available_qty, max_qty_per_order, inception_ts, expiration_ts, vat, vat_type, price_cts, src_price_cts, service_type, supplement_policy) \ - values(:eventId, :fixPrice, :ordinal, :availableQty, :maxQtyPerOrder, :inceptionTs, :expirationTs, :vat, :vatType, 0, :srcPriceCts, :type, :supplementPolicy)\ + insert into additional_service (event_id_fk, fix_price, ordinal, available_qty, max_qty_per_order, inception_ts, expiration_ts, vat, vat_type, price_cts, src_price_cts, service_type, supplement_policy, price_min_cts, price_max_cts) \ + values(:eventId, :fixPrice, :ordinal, :availableQty, :maxQtyPerOrder, :inceptionTs, :expirationTs, :vat, :vatType, 0, :srcPriceCts, :type, :supplementPolicy, :minPriceCts, :maxPriceCts)\ """) @AutoGeneratedKey("id") AffectedRowCountAndKey insert(@Bind("eventId") int eventId, @Bind("srcPriceCts") int srcPriceCts, @Bind("fixPrice") boolean fixPrice, @@ -68,7 +68,9 @@ AffectedRowCountAndKey insert(@Bind("eventId") int eventId, @Bind("srcP @Bind("inceptionTs") ZonedDateTime inception, @Bind("expirationTs") ZonedDateTime expiration, @Bind("vat") BigDecimal vat, @Bind("vatType") AdditionalService.VatType vatType, @Bind("type")AdditionalService.AdditionalServiceType type, - @Bind("supplementPolicy") AdditionalService.SupplementPolicy supplementPolicy); + @Bind("supplementPolicy") AdditionalService.SupplementPolicy supplementPolicy, + @Bind("minPriceCts") Integer minPriceCts, + @Bind("maxPriceCts") Integer maxPriceCts); @Query(""" update additional_service set fix_price = :fixPrice, ordinal = :ordinal, available_qty = :availableQty, max_qty_per_order = :maxQtyPerOrder,\ diff --git a/src/main/java/alfio/util/MonetaryUtil.java b/src/main/java/alfio/util/MonetaryUtil.java index 585d11c623..b376800e47 100644 --- a/src/main/java/alfio/util/MonetaryUtil.java +++ b/src/main/java/alfio/util/MonetaryUtil.java @@ -114,4 +114,13 @@ public static String formatUnit(BigDecimal unit, String currencyCode) { var currencyUnit = CurrencyUnit.of(Objects.requireNonNull(currencyCode).toUpperCase(Locale.ENGLISH)); return Objects.requireNonNull(unit).setScale(currencyUnit.getDecimalPlaces(), HALF_UP).toPlainString(); } + + public static String formatPercentage(int percentageCts) { + return formatPercentage(new BigDecimal(percentageCts).divide(HUNDRED, HALF_UP) + .setScale(2, HALF_UP)); + } + + public static String formatPercentage(BigDecimal unit) { + return unit.stripTrailingZeros().toPlainString(); + } } diff --git a/src/main/java/alfio/util/ReservationUtil.java b/src/main/java/alfio/util/ReservationUtil.java index 18aaf9d2ca..f7e7ebaf15 100644 --- a/src/main/java/alfio/util/ReservationUtil.java +++ b/src/main/java/alfio/util/ReservationUtil.java @@ -113,8 +113,8 @@ public static Optional, return as.getInception(event.getZoneId()).isBefore(now) && as.getExpiration(event.getZoneId()).isAfter(now) && asm.getQuantity() >= 0 && - ((as.isFixPrice() && asm.isQuantityValid(as, selectionCount)) || (!as.isFixPrice() && asm.getAmount() != null && asm.getAmount().compareTo(BigDecimal.ZERO) >= 0)) && - eventManager.eventExistsById(as.getEventId()); + ((as.fixPrice() && asm.isQuantityValid(as, selectionCount)) || (!as.fixPrice() && asm.getAmount() != null && asm.getAmount().compareTo(BigDecimal.ZERO) >= 0)) && + eventManager.eventExistsById(as.eventId()); }); if(!validCategorySelection || !validAdditionalServiceSelected) { diff --git a/src/main/java/alfio/util/Validator.java b/src/main/java/alfio/util/Validator.java index cae94de252..0fb10b0a40 100644 --- a/src/main/java/alfio/util/Validator.java +++ b/src/main/java/alfio/util/Validator.java @@ -67,7 +67,9 @@ public final class Validator { private static final String ADDITIONAL_PREFIX = "additional["; private static final String ADDITIONAL_SERVICES = "additionalServices"; private static final String ERROR_RESTRICTED_VALUE = "error.restrictedValue"; - public static final Supplier LOCAL_DATE_SUPPLIER = () -> LocalDate.now(ClockProvider.clock()); + private static final Supplier LOCAL_DATE_SUPPLIER = () -> LocalDate.now(ClockProvider.clock()); + private static final String DESCRIPTION = "description"; + private static final String TITLE = "title"; private Validator() { } @@ -94,7 +96,7 @@ public static ValidationResult validateEventHeader(Optional event, EventM var descriptions = ev.getDescription(); if(descriptions == null || descriptions.values().stream().anyMatch(v -> v == null || v.isBlank() || v.length() > descriptionMaxLength)) { - errors.rejectValue("description", ERROR_DESCRIPTION); + errors.rejectValue(DESCRIPTION, ERROR_DESCRIPTION); } ValidationUtils.rejectIfEmptyOrWhitespace(errors, "termsAndConditionsUrl", "error.termsandconditionsurl"); @@ -178,7 +180,7 @@ public static ValidationResult validateCategory(TicketCategoryModification categ errors.rejectValue(prefix + "expiration", "error.date.overflow"); } if(isCategoryDescriptionTooLong(category, descriptionMaxLength)) { - errors.rejectValue(prefix + "description", ERROR_DESCRIPTION); + errors.rejectValue(prefix + DESCRIPTION, ERROR_DESCRIPTION); } return evaluateValidationResult(errors); } @@ -608,37 +610,43 @@ public static ValidationResult validateAdditionalService(EventModification.Addit } public static ValidationResult validateAdditionalService(EventModification.AdditionalService additionalService, EventModification eventModification, Errors errors) { + boolean eventModificationIsNotNull = eventModification != null; if(additionalService.isFixPrice() && Optional.ofNullable(additionalService.getPrice()).filter(p -> p.compareTo(BigDecimal.ZERO) >= 0).isEmpty()) { - errors.rejectValue(ADDITIONAL_SERVICES, "error.price"); + rejectFieldForAdditionalService("price", eventModificationIsNotNull, errors); } List descriptions = additionalService.getDescription(); List titles = additionalService.getTitle(); if(descriptions == null || titles == null || titles.size() != descriptions.size()) { - errors.rejectValue(ADDITIONAL_SERVICES, "error.title"); - errors.rejectValue(ADDITIONAL_SERVICES, ERROR_DESCRIPTION); + rejectFieldForAdditionalService(TITLE, eventModificationIsNotNull, errors); + rejectFieldForAdditionalService(DESCRIPTION, eventModificationIsNotNull, errors); } else { if(!validateDescriptionList(titles) || !containsAllRequiredTranslations(eventModification, titles)) { - errors.rejectValue(ADDITIONAL_SERVICES, "error.title"); + rejectFieldForAdditionalService(TITLE, eventModificationIsNotNull, errors); } if(!validateDescriptionList(descriptions) || !containsAllRequiredTranslations(eventModification, descriptions)) { - errors.rejectValue(ADDITIONAL_SERVICES, ERROR_DESCRIPTION); + rejectFieldForAdditionalService(DESCRIPTION, eventModificationIsNotNull, errors); } } DateTimeModification inception = additionalService.getInception(); DateTimeModification expiration = additionalService.getExpiration(); if(inception == null || expiration == null || expiration.isBefore(inception)) { - errors.rejectValue(ADDITIONAL_SERVICES, "error.inception"); - errors.rejectValue(ADDITIONAL_SERVICES, "error.expiration"); + rejectFieldForAdditionalService("inception", eventModificationIsNotNull, errors); + rejectFieldForAdditionalService("expiration", eventModificationIsNotNull, errors); } else if(eventModification != null && expiration.isAfter(eventModification.getEnd())) { - errors.rejectValue(ADDITIONAL_SERVICES, "error.expiration"); + rejectFieldForAdditionalService("expiration", true, errors); } return evaluateValidationResult(errors); } + private static void rejectFieldForAdditionalService(String field, boolean eventModification, Errors errors) { + var errorField = eventModification ? ADDITIONAL_SERVICES : field; + errors.rejectValue(errorField, "error."+field); + } + private static boolean containsAllRequiredTranslations(EventModification eventModification, List descriptions) { Optional optional = Optional.ofNullable(eventModification); return optional.isEmpty() || diff --git a/src/main/resources/alfio/db/PGSQL/V205_2.0.0.56__PERCENTAGE_ADDITIONAL_ITEMS.sql b/src/main/resources/alfio/db/PGSQL/V205_2.0.0.56__PERCENTAGE_ADDITIONAL_ITEMS.sql new file mode 100644 index 0000000000..d5b998e60d --- /dev/null +++ b/src/main/resources/alfio/db/PGSQL/V205_2.0.0.56__PERCENTAGE_ADDITIONAL_ITEMS.sql @@ -0,0 +1,21 @@ +-- +-- This file is part of alf.io. +-- +-- alf.io is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- alf.io is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with alf.io. If not, see . +-- + +alter table additional_service + add column price_min_cts integer, + add column price_max_cts integer; + diff --git a/src/main/resources/alfio/db/PGSQL/afterMigrate__008_VIEW_additional_service_with_currency.sql b/src/main/resources/alfio/db/PGSQL/afterMigrate__008_VIEW_additional_service_with_currency.sql index 51e6b8f97d..5c42f786d9 100644 --- a/src/main/resources/alfio/db/PGSQL/afterMigrate__008_VIEW_additional_service_with_currency.sql +++ b/src/main/resources/alfio/db/PGSQL/afterMigrate__008_VIEW_additional_service_with_currency.sql @@ -34,7 +34,9 @@ create view additional_service_with_currency as ( asv.supplement_policy supplement_policy, asv.organization_id_fk organization_id_fk, e.currency currency_code, - (case when asv.available_qty > 0 then (select count(*) from additional_service_item where additional_service_id_fk = asv.id and status = 'FREE') else 999 end) as available_count + (case when asv.available_qty > 0 then (select count(*) from additional_service_item where additional_service_id_fk = asv.id and status = 'FREE') else 999 end) as available_count, + asv.price_min_cts price_min_cts, + asv.price_max_cts price_max_cts from additional_service asv, event e where asv.event_id_fk = e.id ) \ No newline at end of file diff --git a/src/main/resources/alfio/i18n/public.properties b/src/main/resources/alfio/i18n/public.properties index 2ee2dcea4e..51e84f79b8 100644 --- a/src/main/resources/alfio/i18n/public.properties +++ b/src/main/resources/alfio/i18n/public.properties @@ -39,6 +39,8 @@ show-subscription.header.title=Buy a {0} subscription show-event.tickets.left={0} left show-event.category.quantity=Quantity show-event.additional.custom-amount=Amount +show-event.additional.percentage.tickets=of tickets price +show-event.additional.percentage.reservation=of reservation price show-event.by=By show-event.tickets=Tickets show-event.additional-services=Additional options @@ -63,6 +65,7 @@ show-event.promo-code-fixed-amount-discount={0} discount for each ticket show-event.add-to-calendar=Add to calendar show-event.expired-categories=Expired categories: show-event.mandatoryOneForTicket=Surcharge, 1 per ticket +show-event.mandatoryPercentage=Surcharge #reservation-page.ms reservation-page.header.title=Order summary for {0} diff --git a/src/main/resources/alfio/web-templates/admin-index.ms b/src/main/resources/alfio/web-templates/admin-index.ms index 06c61445dd..b198caa844 100644 --- a/src/main/resources/alfio/web-templates/admin-index.ms +++ b/src/main/resources/alfio/web-templates/admin-index.ms @@ -27,11 +27,11 @@ - @@ -48,7 +48,6 @@ - @@ -231,7 +230,6 @@
-
+ \ No newline at end of file diff --git a/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/.bower.json b/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/.bower.json deleted file mode 100644 index 853de1ad61..0000000000 --- a/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/.bower.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "author": [ - "Marco Rinck", - "Jan Stevens", - "Silvan van Leeuwen" - ], - "name": "angular-growl-v2", - "description": "growl like notifications for angularJS projects, using bootstrap alert classes.", - "version": "0.7.9", - "homepage": "http://janstevens.github.io/angular-growl-2", - "repository": { - "type": "git", - "url": "https://github.com/Swilvan/angular-growl-2" - }, - "license": "MIT", - "main": [ - "./build/angular-growl.js", - "./build/angular-growl.css" - ], - "ignore": [ - ".jshintrc", - ".gitignore", - "README.md", - "CHANGELOG.md", - "package.json", - "gruntfile.js", - "karma.conf.js", - "doc", - "src", - "test", - "demo" - ], - "dependencies": { - "angular": ">=1.2.1" - }, - "devDependencies": { - "angular-mocks": ">=1.2.1" - }, - "_release": "0.7.9", - "_resolution": { - "type": "version", - "tag": "v0.7.9", - "commit": "255df72deadf61f7b244d3c8a1ec274aac7c1774" - }, - "_source": "https://github.com/JanStevens/angular-growl-2.git", - "_target": "^0.7.9", - "_originalSource": "angular-growl-v2", - "_direct": true -} \ No newline at end of file diff --git a/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/.gitignore b/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/.gitignore deleted file mode 100644 index 684ae5d80c..0000000000 --- a/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/.gitignore +++ /dev/null @@ -1 +0,0 @@ -!build \ No newline at end of file diff --git a/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/LICENSE b/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/LICENSE deleted file mode 100644 index 3db10d4782..0000000000 --- a/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 Marco Rinck - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file diff --git a/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/bower.json b/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/bower.json deleted file mode 100644 index 48885582d6..0000000000 --- a/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/bower.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "author": [ - "Marco Rinck", - "Jan Stevens", - "Silvan van Leeuwen" - ], - "name": "angular-growl-v2", - "description": "growl like notifications for angularJS projects, using bootstrap alert classes.", - "version": "0.7.9", - "homepage": "http://janstevens.github.io/angular-growl-2", - "repository": { - "type": "git", - "url": "https://github.com/Swilvan/angular-growl-2" - }, - "license": "MIT", - "main": ["./build/angular-growl.js", "./build/angular-growl.css"], - "ignore": [ - ".jshintrc", - ".gitignore", - "README.md", - "CHANGELOG.md", - "package.json", - "gruntfile.js", - "karma.conf.js", - "doc", - "src", - "test", - "demo" - ], - "dependencies": { - "angular": ">=1.2.1" - }, - "devDependencies": { - "angular-mocks": ">=1.2.1" - } -} diff --git a/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/build/angular-growl.css b/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/build/angular-growl.css deleted file mode 100644 index b50e10d2d0..0000000000 --- a/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/build/angular-growl.css +++ /dev/null @@ -1,135 +0,0 @@ -/** - * angular-growl-v2 - v0.7.8 - 2015-10-25 - * http://janstevens.github.io/angular-growl-2 - * Copyright (c) 2015 Marco Rinck,Jan Stevens,Silvan van Leeuwen; Licensed MIT - */ -/* - * growl-container styles - */ -.growl-container.growl-fixed { - position: fixed; - float: right; - width: 90%; - max-width: 400px; - z-index: 9999; -} -.growl-container.growl-fixed.top-right { - top: 10px; - right: 15px; -} -.growl-container.growl-fixed.bottom-right { - bottom: 10px; - right: 15px; -} -.growl-container.growl-fixed.middle-right { - top: 49%; - right: 15px; -} -.growl-container.growl-fixed.top-left { - top: 10px; - left: 15px; -} -.growl-container.growl-fixed.bottom-left { - bottom: 10px; - left: 15px; -} -.growl-container.growl-fixed.middle-left { - top: 49%; - left: 15px; -} -.growl-container.growl-fixed.top-center { - top: 10px; - left: 50%; - margin-left: -200px; -} -.growl-container.growl-fixed.bottom-center { - bottom: 10px; - left: 50%; - margin-left: -200px; -} -.growl-container.growl-fixed.middle-center { - top: 49%; - left: 50%; - margin-left: -200px; -} - -/* - * growl-item styles - */ -.growl-container > .growl-item { - padding: 10px; - padding-right: 35px; - margin-bottom: 10px; - cursor: pointer; -} - -.growl-container > button { - border: none; - outline:none; -} -.growl-container > .growl-item.ng-enter, -.growl-container > .growl-item.ng-leave { - -webkit-transition:0.5s linear all; - -moz-transition:0.5s linear all; - -o-transition:0.5s linear all; - transition:0.5s linear all; -} - -.growl-container > .growl-item.ng-enter, -.growl-container > .growl-item.ng-leave.ng-leave-active { - opacity:0; -} -.growl-container > .growl-item.ng-leave, -.growl-container > .growl-item.ng-enter.ng-enter-active { - opacity:1; -} - -.growl-container > div.growl-item { - background-position: 12px center; - background-repeat: no-repeat; -} - -/* - * growl-title styles - */ -.growl-title { - font-size: 16px; -} -.growl-item.icon > .growl-title { - margin: 0 0 0 40px; -} - -/* - * growl-message styles - */ -.growl-item.icon > .growl-message { - margin: 0 0 0 40px; -} - -/* - * growl background images - */ -.growl-container > .alert-info.icon { - /* for the white images - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII="); - */ - background-image: url("data:image/png;base64,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"); -} -.growl-container > .alert-error.icon { - /* for the white images - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII="); - */ - background-image: url("data:image/png;base64,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"); -} -.growl-container > .alert-success.icon { - /* for the white images - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg=="); - */ - background-image: url("data:image/png;base64,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"); -} -.growl-container > .alert-warning.icon { - /* for the white images - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII="); - */ - background-image: url("data:image/png;base64,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"); -} diff --git a/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/build/angular-growl.js b/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/build/angular-growl.js deleted file mode 100644 index 8e5f818e9a..0000000000 --- a/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/build/angular-growl.js +++ /dev/null @@ -1,431 +0,0 @@ -/** - * angular-growl-v2 - v0.7.8 - 2015-10-25 - * http://janstevens.github.io/angular-growl-2 - * Copyright (c) 2015 Marco Rinck,Jan Stevens,Silvan van Leeuwen; Licensed MIT - */ -angular.module('angular-growl', []); -angular.module('angular-growl').directive('growl', [function () { - 'use strict'; - return { - restrict: 'A', - templateUrl: 'templates/growl/growl.html', - replace: false, - scope: { - reference: '@', - inline: '=', - limitMessages: '=' - }, - controller: [ - '$scope', - '$interval', - 'growl', - 'growlMessages', - function ($scope, $interval, growl, growlMessages) { - $scope.referenceId = $scope.reference || 0; - growlMessages.initDirective($scope.referenceId, $scope.limitMessages); - $scope.growlMessages = growlMessages; - $scope.inlineMessage = angular.isDefined($scope.inline) ? $scope.inline : growl.inlineMessages(); - $scope.$watch('limitMessages', function (limitMessages) { - var directive = growlMessages.directives[$scope.referenceId]; - if (!angular.isUndefined(limitMessages) && !angular.isUndefined(directive)) { - directive.limitMessages = limitMessages; - } - }); - $scope.stopTimeoutClose = function (message) { - if (!message.clickToClose) { - angular.forEach(message.promises, function (promise) { - $interval.cancel(promise); - }); - if (message.close) { - growlMessages.deleteMessage(message); - } else { - message.close = true; - } - } - }; - $scope.alertClasses = function (message) { - return { - 'alert-success': message.severity === 'success', - 'alert-error': message.severity === 'error', - 'alert-danger': message.severity === 'error', - 'alert-info': message.severity === 'info', - 'alert-warning': message.severity === 'warning', - 'icon': message.disableIcons === false, - 'alert-dismissable': !message.disableCloseButton - }; - }; - $scope.showCountDown = function (message) { - return !message.disableCountDown && message.ttl > 0; - }; - $scope.wrapperClasses = function () { - var classes = {}; - classes['growl-fixed'] = !$scope.inlineMessage; - classes[growl.position()] = true; - return classes; - }; - $scope.computeTitle = function (message) { - var ret = { - 'success': 'Success', - 'error': 'Error', - 'info': 'Information', - 'warn': 'Warning' - }; - return ret[message.severity]; - }; - } - ] - }; - }]); -angular.module('angular-growl').run([ - '$templateCache', - function ($templateCache) { - 'use strict'; - if ($templateCache.get('templates/growl/growl.html') === undefined) { - $templateCache.put('templates/growl/growl.html', '
' + '
' + '' + '' + '

' + '
' + '
' + '
'); - } - } -]); -angular.module('angular-growl').provider('growl', function () { - 'use strict'; - var _ttl = { - success: null, - error: null, - warning: null, - info: null - }, _messagesKey = 'messages', _messageTextKey = 'text', _messageTitleKey = 'title', _messageSeverityKey = 'severity', _messageTTLKey = 'ttl', _onlyUniqueMessages = true, _messageVariableKey = 'variables', _referenceId = 0, _inline = false, _position = 'top-right', _disableCloseButton = false, _disableIcons = false, _reverseOrder = false, _disableCountDown = false, _translateMessages = true; - this.globalTimeToLive = function (ttl) { - if (typeof ttl === 'object') { - for (var k in ttl) { - if (ttl.hasOwnProperty(k)) { - _ttl[k] = ttl[k]; - } - } - } else { - for (var severity in _ttl) { - if (_ttl.hasOwnProperty(severity)) { - _ttl[severity] = ttl; - } - } - } - return this; - }; - this.globalTranslateMessages = function (translateMessages) { - _translateMessages = translateMessages; - return this; - }; - this.globalDisableCloseButton = function (disableCloseButton) { - _disableCloseButton = disableCloseButton; - return this; - }; - this.globalDisableIcons = function (disableIcons) { - _disableIcons = disableIcons; - return this; - }; - this.globalReversedOrder = function (reverseOrder) { - _reverseOrder = reverseOrder; - return this; - }; - this.globalDisableCountDown = function (countDown) { - _disableCountDown = countDown; - return this; - }; - this.messageVariableKey = function (messageVariableKey) { - _messageVariableKey = messageVariableKey; - return this; - }; - this.globalInlineMessages = function (inline) { - _inline = inline; - return this; - }; - this.globalPosition = function (position) { - _position = position; - return this; - }; - this.messagesKey = function (messagesKey) { - _messagesKey = messagesKey; - return this; - }; - this.messageTextKey = function (messageTextKey) { - _messageTextKey = messageTextKey; - return this; - }; - this.messageTitleKey = function (messageTitleKey) { - _messageTitleKey = messageTitleKey; - return this; - }; - this.messageSeverityKey = function (messageSeverityKey) { - _messageSeverityKey = messageSeverityKey; - return this; - }; - this.messageTTLKey = function (messageTTLKey) { - _messageTTLKey = messageTTLKey; - return this; - }; - this.onlyUniqueMessages = function (onlyUniqueMessages) { - _onlyUniqueMessages = onlyUniqueMessages; - return this; - }; - this.serverMessagesInterceptor = [ - '$q', - 'growl', - function ($q, growl) { - function checkResponse(response) { - if (response !== undefined && response.data && response.data[_messagesKey] && response.data[_messagesKey].length > 0) { - growl.addServerMessages(response.data[_messagesKey]); - } - } - return { - 'response': function (response) { - checkResponse(response); - return response; - }, - 'responseError': function (rejection) { - checkResponse(rejection); - return $q.reject(rejection); - } - }; - } - ]; - this.$get = [ - '$rootScope', - '$interpolate', - '$sce', - '$filter', - '$interval', - 'growlMessages', - function ($rootScope, $interpolate, $sce, $filter, $interval, growlMessages) { - var translate; - growlMessages.onlyUnique = _onlyUniqueMessages; - growlMessages.reverseOrder = _reverseOrder; - try { - translate = $filter('translate'); - } catch (e) { - } - function broadcastMessage(message) { - if (translate && message.translateMessage) { - message.text = translate(message.text, message.variables) || message.text; - message.title = translate(message.title) || message.title; - } else { - var polation = $interpolate(message.text); - message.text = polation(message.variables); - } - var addedMessage = growlMessages.addMessage(message); - $rootScope.$broadcast('growlMessage', message); - $interval(function () { - }, 0, 1); - return addedMessage; - } - function sendMessage(text, config, severity) { - var _config = config || {}, message; - message = { - text: text, - title: _config.title, - severity: severity, - ttl: _config.ttl || _ttl[severity], - variables: _config.variables || {}, - disableCloseButton: _config.disableCloseButton === undefined ? _disableCloseButton : _config.disableCloseButton, - disableIcons: _config.disableIcons === undefined ? _disableIcons : _config.disableIcons, - disableCountDown: _config.disableCountDown === undefined ? _disableCountDown : _config.disableCountDown, - position: _config.position || _position, - referenceId: _config.referenceId || _referenceId, - translateMessage: _config.translateMessage === undefined ? _translateMessages : _config.translateMessage, - destroy: function () { - growlMessages.deleteMessage(message); - }, - setText: function (newText) { - message.text = $sce.trustAsHtml(String(newText)); - }, - onclose: _config.onclose, - onopen: _config.onopen - }; - return broadcastMessage(message); - } - function warning(text, config) { - return sendMessage(text, config, 'warning'); - } - function error(text, config) { - return sendMessage(text, config, 'error'); - } - function info(text, config) { - return sendMessage(text, config, 'info'); - } - function success(text, config) { - return sendMessage(text, config, 'success'); - } - function general(text, config, severity) { - severity = (severity || 'error').toLowerCase(); - return sendMessage(text, config, severity); - } - function addServerMessages(messages) { - if (!messages || !messages.length) { - return; - } - var i, message, severity, length; - length = messages.length; - for (i = 0; i < length; i++) { - message = messages[i]; - if (message[_messageTextKey]) { - severity = (message[_messageSeverityKey] || 'error').toLowerCase(); - var config = {}; - config.variables = message[_messageVariableKey] || {}; - config.title = message[_messageTitleKey]; - if (message[_messageTTLKey]) { - config.ttl = message[_messageTTLKey]; - } - sendMessage(message[_messageTextKey], config, severity); - } - } - } - function onlyUnique() { - return _onlyUniqueMessages; - } - function reverseOrder() { - return _reverseOrder; - } - function inlineMessages() { - return _inline; - } - function position() { - return _position; - } - return { - warning: warning, - error: error, - info: info, - success: success, - general: general, - addServerMessages: addServerMessages, - onlyUnique: onlyUnique, - reverseOrder: reverseOrder, - inlineMessages: inlineMessages, - position: position - }; - } - ]; -}); -angular.module('angular-growl').service('growlMessages', [ - '$sce', - '$interval', - function ($sce, $interval) { - 'use strict'; - var self = this; - this.directives = {}; - var preloadDirectives = {}; - function preLoad(referenceId) { - var directive; - if (preloadDirectives[referenceId]) { - directive = preloadDirectives[referenceId]; - } else { - directive = preloadDirectives[referenceId] = { messages: [] }; - } - return directive; - } - function directiveForRefId(referenceId) { - var refId = referenceId || 0; - return self.directives[refId] || preloadDirectives[refId]; - } - this.initDirective = function (referenceId, limitMessages) { - if (preloadDirectives[referenceId]) { - this.directives[referenceId] = preloadDirectives[referenceId]; - this.directives[referenceId].limitMessages = limitMessages; - } else { - this.directives[referenceId] = { - messages: [], - limitMessages: limitMessages - }; - } - return this.directives[referenceId]; - }; - this.getAllMessages = function (referenceId) { - referenceId = referenceId || 0; - var messages; - if (directiveForRefId(referenceId)) { - messages = directiveForRefId(referenceId).messages; - } else { - messages = []; - } - return messages; - }; - this.destroyAllMessages = function (referenceId) { - var messages = this.getAllMessages(referenceId); - for (var i = messages.length - 1; i >= 0; i--) { - messages[i].destroy(); - } - var directive = directiveForRefId(referenceId); - if (directive) { - directive.messages = []; - } - }; - this.addMessage = function (message) { - var directive, messages, found, msgText; - if (this.directives[message.referenceId]) { - directive = this.directives[message.referenceId]; - } else { - directive = preLoad(message.referenceId); - } - messages = directive.messages; - if (this.onlyUnique) { - angular.forEach(messages, function (msg) { - msgText = $sce.getTrustedHtml(msg.text); - if (message.text === msgText && message.severity === msg.severity && message.title === msg.title) { - found = true; - } - }); - if (found) { - return; - } - } - message.text = $sce.trustAsHtml(String(message.text)); - if (message.ttl && message.ttl !== -1) { - message.countdown = message.ttl / 1000; - message.promises = []; - message.close = false; - message.countdownFunction = function () { - if (message.countdown > 1) { - message.countdown--; - message.promises.push($interval(message.countdownFunction, 1000, 1, 1)); - } else { - message.countdown--; - } - }; - } - if (angular.isDefined(directive.limitMessages)) { - var diff = messages.length - (directive.limitMessages - 1); - if (diff > 0) { - messages.splice(directive.limitMessages - 1, diff); - } - } - if (this.reverseOrder) { - messages.unshift(message); - } else { - messages.push(message); - } - if (typeof message.onopen === 'function') { - message.onopen(); - } - if (message.ttl && message.ttl !== -1) { - var self = this; - message.promises.push($interval(angular.bind(this, function () { - self.deleteMessage(message); - }), message.ttl, 1, 1)); - message.promises.push($interval(message.countdownFunction, 1000, 1, 1)); - } - return message; - }; - this.deleteMessage = function (message) { - var messages = this.getAllMessages(message.referenceId), index = -1; - for (var i in messages) { - if (messages.hasOwnProperty(i)) { - index = messages[i] === message ? i : index; - } - } - if (index > -1) { - messages[index].close = true; - messages.splice(index, 1); - } - if (typeof message.onclose === 'function') { - message.onclose(); - } - }; - } -]); \ No newline at end of file diff --git a/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/build/angular-growl.min.css b/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/build/angular-growl.min.css deleted file mode 100644 index 1016b02e66..0000000000 --- a/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/build/angular-growl.min.css +++ /dev/null @@ -1,7 +0,0 @@ -/** - * angular-growl-v2 - v0.7.5 - 2015-10-02 - * http://janstevens.github.io/angular-growl-2 - * Copyright (c) 2015 Marco Rinck,Jan Stevens,Silvan van Leeuwen; Licensed MIT - */ - -.growl-container.growl-fixed{position:fixed;float:right;width:90%;max-width:400px;z-index:9999}.growl-container.growl-fixed.top-right{top:10px;right:15px}.growl-container.growl-fixed.bottom-right{bottom:10px;right:15px}.growl-container.growl-fixed.middle-right{top:49%;right:15px}.growl-container.growl-fixed.top-left{top:10px;left:15px}.growl-container.growl-fixed.bottom-left{bottom:10px;left:15px}.growl-container.growl-fixed.middle-left{top:49%;left:15px}.growl-container.growl-fixed.top-center{top:10px;left:50%;margin-left:-200px}.growl-container.growl-fixed.bottom-center{bottom:10px;left:50%;margin-left:-200px}.growl-container.growl-fixed.middle-center{top:49%;left:50%;margin-left:-200px}.growl-container>.growl-item{padding:10px;padding-right:35px;margin-bottom:10px;cursor:pointer}.growl-container>button{border:0;outline:0}.growl-container>.growl-item.ng-enter,.growl-container>.growl-item.ng-leave{-webkit-transition:.5s linear all;-moz-transition:.5s linear all;-o-transition:.5s linear all;transition:.5s linear all}.growl-container>.growl-item.ng-enter,.growl-container>.growl-item.ng-leave.ng-leave-active{opacity:0}.growl-container>.growl-item.ng-leave,.growl-container>.growl-item.ng-enter.ng-enter-active{opacity:1}.growl-container>div.growl-item{background-position:12px center;background-repeat:no-repeat}.growl-title{font-size:16px}.growl-item.icon>.growl-title{margin:0 0 0 40px}.growl-item.icon>.growl-message{margin:0 0 0 40px}.growl-container>.alert-info.icon{background-image:url("data:image/png;base64,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")}.growl-container>.alert-error.icon{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAwBJREFUeNqsll1IU2EYx5+zj7Ozdo477sOPuZiJ+Nlm2CRZ6yJKJcGICDRvEjY2+rg0EIkuxLzRC+lGEUZGRJd1EQahoikUiBVZF64hzAYqhlvbztzZOdvpavNsunOm9b96X97nfX7P877ned6DcBwHCIKAkNY8HlMkEHge9vutusbGSUyrfXS2ry8OIuI4DpB8gDWPx8RQVHd8b69rZ3X1IkNRGSOUIFKaurqfp0pK3spx/F2T2z0nCFgaHPzBJZNokqZVSZpWJsJhFbW9LYcCJVepOFV5eUSmVFJShYJi9/eJFMMork5MFMsAAP5sbNTs7+7KxBzJMIxj4/FD6TIUhYR8viIAKMpdkwAAKNTqmJBjRCqFM52dkzdnZiR1PT2XNPX1vwvNTgIAIJHJWCGjIpMp2tLffxcAwOJyLWtqa0fFHEtR9AAgx/E9wShyA0CQhBgAVatZfgYx+M9CCSKeAcgwbFfIOB4M4scG4HgoA0DV6gUh4xTDSPjzJE1XiQGUOt3XAwCOvxAypkOhLECKZXUF1MbnDMDscPjxioq4WLvgZWAQBRDEywwAAAA3GjcEj4llG9NjNhYrE7Ilq6vDFqfTmwVQarVzQpsSkUhXZhyNakUAbzLVz7uUxwqSvJ973mkFvd7eldFReZKmjZvz8zqhqsc0mieZOb+bfhwamv21sHDlX75/vcUSuDw+fjrdTbOiVRkM99IlfhJJURS0DQ23D/WitCxOp7fUav1y1ObimpqQqb39lcFm+yLDMO4om/LW1mWLy7WcFwAAoK6svKbU69ncyMpaWs5dGBjotQ8PNxtstve5+zCNJkkYjbeO7KZ8mZ3OnQq7/WFW5SYSwFBUNwDA9+lpMrq1dZ6/riDJlKmt7brZ6dw5dOn5nsxPIyOvN2dnb2SVv17PMpGIlP/oKEgyVdnR0dXkds8c603OB8mNPJ/zggAAACtjY08Di4sP+I++XKXiSq3WD4TReMfscPhP9FfB17epKXvQ53sWXF+vKmluXhJzzAf8HQAC8i1VVpW6ogAAAABJRU5ErkJggg==")}.growl-container>.alert-success.icon{background-image:url("data:image/png;base64,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")}.growl-container>.alert-warning.icon{background-image:url("data:image/png;base64,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")} \ No newline at end of file diff --git a/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/build/angular-growl.min.js b/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/build/angular-growl.min.js deleted file mode 100644 index 42b604d4a1..0000000000 --- a/src/main/webapp/alfio-admin-v1/bower_components/angular-growl-v2/build/angular-growl.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * angular-growl-v2 - v0.7.8 - 2015-10-25 - * http://janstevens.github.io/angular-growl-2 - * Copyright (c) 2015 Marco Rinck,Jan Stevens,Silvan van Leeuwen; Licensed MIT - */ -angular.module("angular-growl",[]),angular.module("angular-growl").directive("growl",[function(){"use strict";return{restrict:"A",templateUrl:"templates/growl/growl.html",replace:!1,scope:{reference:"@",inline:"=",limitMessages:"="},controller:["$scope","$interval","growl","growlMessages",function(a,b,c,d){a.referenceId=a.reference||0,d.initDirective(a.referenceId,a.limitMessages),a.growlMessages=d,a.inlineMessage=angular.isDefined(a.inline)?a.inline:c.inlineMessages(),a.$watch("limitMessages",function(b){var c=d.directives[a.referenceId];angular.isUndefined(b)||angular.isUndefined(c)||(c.limitMessages=b)}),a.stopTimeoutClose=function(a){a.clickToClose||(angular.forEach(a.promises,function(a){b.cancel(a)}),a.close?d.deleteMessage(a):a.close=!0)},a.alertClasses=function(a){return{"alert-success":"success"===a.severity,"alert-error":"error"===a.severity,"alert-danger":"error"===a.severity,"alert-info":"info"===a.severity,"alert-warning":"warning"===a.severity,icon:a.disableIcons===!1,"alert-dismissable":!a.disableCloseButton}},a.showCountDown=function(a){return!a.disableCountDown&&a.ttl>0},a.wrapperClasses=function(){var b={};return b["growl-fixed"]=!a.inlineMessage,b[c.position()]=!0,b},a.computeTitle=function(a){var b={success:"Success",error:"Error",info:"Information",warn:"Warning"};return b[a.severity]}}]}}]),angular.module("angular-growl").run(["$templateCache",function(a){"use strict";void 0===a.get("templates/growl/growl.html")&&a.put("templates/growl/growl.html",'

')}]),angular.module("angular-growl").provider("growl",function(){"use strict";var a={success:null,error:null,warning:null,info:null},b="messages",c="text",d="title",e="severity",f="ttl",g=!0,h="variables",i=0,j=!1,k="top-right",l=!1,m=!1,n=!1,o=!1,p=!0;this.globalTimeToLive=function(b){if("object"==typeof b)for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c]);else for(var d in a)a.hasOwnProperty(d)&&(a[d]=b);return this},this.globalTranslateMessages=function(a){return p=a,this},this.globalDisableCloseButton=function(a){return l=a,this},this.globalDisableIcons=function(a){return m=a,this},this.globalReversedOrder=function(a){return n=a,this},this.globalDisableCountDown=function(a){return o=a,this},this.messageVariableKey=function(a){return h=a,this},this.globalInlineMessages=function(a){return j=a,this},this.globalPosition=function(a){return k=a,this},this.messagesKey=function(a){return b=a,this},this.messageTextKey=function(a){return c=a,this},this.messageTitleKey=function(a){return d=a,this},this.messageSeverityKey=function(a){return e=a,this},this.messageTTLKey=function(a){return f=a,this},this.onlyUniqueMessages=function(a){return g=a,this},this.serverMessagesInterceptor=["$q","growl",function(a,c){function d(a){void 0!==a&&a.data&&a.data[b]&&a.data[b].length>0&&c.addServerMessages(a.data[b])}return{response:function(a){return d(a),a},responseError:function(b){return d(b),a.reject(b)}}}],this.$get=["$rootScope","$interpolate","$sce","$filter","$interval","growlMessages",function(b,q,r,s,t,u){function v(a){if(H&&a.translateMessage)a.text=H(a.text,a.variables)||a.text,a.title=H(a.title)||a.title;else{var c=q(a.text);a.text=c(a.variables)}var d=u.addMessage(a);return b.$broadcast("growlMessage",a),t(function(){},0,1),d}function w(b,c,d){var e,f=c||{};return e={text:b,title:f.title,severity:d,ttl:f.ttl||a[d],variables:f.variables||{},disableCloseButton:void 0===f.disableCloseButton?l:f.disableCloseButton,disableIcons:void 0===f.disableIcons?m:f.disableIcons,disableCountDown:void 0===f.disableCountDown?o:f.disableCountDown,position:f.position||k,referenceId:f.referenceId||i,translateMessage:void 0===f.translateMessage?p:f.translateMessage,destroy:function(){u.deleteMessage(e)},setText:function(a){e.text=r.trustAsHtml(String(a))},onclose:f.onclose,onopen:f.onopen},v(e)}function x(a,b){return w(a,b,"warning")}function y(a,b){return w(a,b,"error")}function z(a,b){return w(a,b,"info")}function A(a,b){return w(a,b,"success")}function B(a,b,c){return c=(c||"error").toLowerCase(),w(a,b,c)}function C(a){if(a&&a.length){var b,g,i,j;for(j=a.length,b=0;j>b;b++)if(g=a[b],g[c]){i=(g[e]||"error").toLowerCase();var k={};k.variables=g[h]||{},k.title=g[d],g[f]&&(k.ttl=g[f]),w(g[c],k,i)}}}function D(){return g}function E(){return n}function F(){return j}function G(){return k}var H;u.onlyUnique=g,u.reverseOrder=n;try{H=s("translate")}catch(I){}return{warning:x,error:y,info:z,success:A,general:B,addServerMessages:C,onlyUnique:D,reverseOrder:E,inlineMessages:F,position:G}}]}),angular.module("angular-growl").service("growlMessages",["$sce","$interval",function(a,b){"use strict";function c(a){var b;return b=f[a]?f[a]:f[a]={messages:[]}}function d(a){var b=a||0;return e.directives[b]||f[b]}var e=this;this.directives={};var f={};this.initDirective=function(a,b){return f[a]?(this.directives[a]=f[a],this.directives[a].limitMessages=b):this.directives[a]={messages:[],limitMessages:b},this.directives[a]},this.getAllMessages=function(a){a=a||0;var b;return b=d(a)?d(a).messages:[]},this.destroyAllMessages=function(a){for(var b=this.getAllMessages(a),c=b.length-1;c>=0;c--)b[c].destroy();var e=d(a);e&&(e.messages=[])},this.addMessage=function(d){var e,f,g,h;if(e=this.directives[d.referenceId]?this.directives[d.referenceId]:c(d.referenceId),f=e.messages,!this.onlyUnique||(angular.forEach(f,function(b){h=a.getTrustedHtml(b.text),d.text===h&&d.severity===b.severity&&d.title===b.title&&(g=!0)}),!g)){if(d.text=a.trustAsHtml(String(d.text)),d.ttl&&-1!==d.ttl&&(d.countdown=d.ttl/1e3,d.promises=[],d.close=!1,d.countdownFunction=function(){d.countdown>1?(d.countdown--,d.promises.push(b(d.countdownFunction,1e3,1,1))):d.countdown--}),angular.isDefined(e.limitMessages)){var i=f.length-(e.limitMessages-1);i>0&&f.splice(e.limitMessages-1,i)}if(this.reverseOrder?f.unshift(d):f.push(d),"function"==typeof d.onopen&&d.onopen(),d.ttl&&-1!==d.ttl){var j=this;d.promises.push(b(angular.bind(this,function(){j.deleteMessage(d)}),d.ttl,1,1)),d.promises.push(b(d.countdownFunction,1e3,1,1))}return d}},this.deleteMessage=function(a){var b=this.getAllMessages(a.referenceId),c=-1;for(var d in b)b.hasOwnProperty(d)&&(c=b[d]===a?d:c);c>-1&&(b[c].close=!0,b.splice(c,1)),"function"==typeof a.onclose&&a.onclose()}}]); \ No newline at end of file diff --git a/src/main/webapp/alfio-admin-v1/js/admin/feature/additional-service/additional-service.js b/src/main/webapp/alfio-admin-v1/js/admin/feature/additional-service/additional-service.js index fa571ee25a..dd72d963a4 100644 --- a/src/main/webapp/alfio-admin-v1/js/admin/feature/additional-service/additional-service.js +++ b/src/main/webapp/alfio-admin-v1/js/admin/feature/additional-service/additional-service.js @@ -335,7 +335,7 @@ return { loadAll: function(eventId) { if(angular.isDefined(eventId)) { - return $http.get('/admin/api/event/'+eventId+'/additional-services/').error(HttpErrorHandler.handle); + return $http.get('/admin/api/event/'+eventId+'/additional-services').error(HttpErrorHandler.handle); } var deferred = $q.defer(); deferred.resolve({data:[]}); @@ -348,7 +348,7 @@ return (angular.isDefined(additionalService.id)) ? this.update(eventId, additionalService) : this.create(eventId, additionalService); }, create: function(eventId, additionalService) { - return $http.post('/admin/api/event/'+eventId+'/additional-services/', additionalService).error(HttpErrorHandler.handle); + return $http.post('/admin/api/event/'+eventId+'/additional-services', additionalService).error(HttpErrorHandler.handle); }, update: function(eventId, additionalService) { return $http['put']('/admin/api/event/'+eventId+'/additional-services/'+additionalService.id, additionalService).error(HttpErrorHandler.handle); diff --git a/src/main/webapp/alfio-admin-v1/js/admin/feature/reservation/reservation.service.js b/src/main/webapp/alfio-admin-v1/js/admin/feature/reservation/reservation.service.js index ca60c9589f..6f243f7f2e 100644 --- a/src/main/webapp/alfio-admin-v1/js/admin/feature/reservation/reservation.service.js +++ b/src/main/webapp/alfio-admin-v1/js/admin/feature/reservation/reservation.service.js @@ -20,7 +20,7 @@ return $http['put']('/admin/api/reservation/'+purchaseContextType+'/'+publicIdentifier+'/'+reservationId+'/notify-attendees', ids).error(HttpErrorHandler.handle); }, load: function(purchaseContextType, publicIdentifier, reservationId) { - return $http.get('/admin/api/reservation/'+purchaseContextType+'/'+publicIdentifier+'/'+reservationId+'/').error(HttpErrorHandler.handle); + return $http.get('/admin/api/reservation/'+purchaseContextType+'/'+publicIdentifier+'/'+reservationId).error(HttpErrorHandler.handle); }, paymentInfo: function(purchaseContextType, publicIdentifier, reservationId) { return $http.get('/admin/api/reservation/'+purchaseContextType+'/'+publicIdentifier+'/'+reservationId+'/payment-info').error(HttpErrorHandler.handle); diff --git a/src/main/webapp/alfio-admin-v1/js/admin/ng-app/admin-application.js b/src/main/webapp/alfio-admin-v1/js/admin/ng-app/admin-application.js index 67e2882f67..48a81a1481 100644 --- a/src/main/webapp/alfio-admin-v1/js/admin/ng-app/admin-application.js +++ b/src/main/webapp/alfio-admin-v1/js/admin/ng-app/admin-application.js @@ -21,7 +21,7 @@ this.loadEvent = loadEvent.data.event; } - admin.config(function($stateProvider, $urlRouterProvider, growlProvider) { + admin.config(function($stateProvider, $urlRouterProvider) { $urlRouterProvider.otherwise("/"); $stateProvider .state('index', { @@ -185,14 +185,14 @@ }) .state('events.single.additionalServices', { url: '/additional-services', - template: '', + template: '', controller: loadEventCtrl, controllerAs: '$ctrl', resolve: loadEvent }) .state('events.single.donations', { url: '/donations', - template: '', + template: '', controller: loadEventCtrl, controllerAs: '$ctrl', resolve: loadEvent @@ -407,8 +407,6 @@ url: '/log', template: '' }); - - growlProvider.globalPosition('bottom-right'); }); navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; diff --git a/src/main/webapp/alfio-admin-v1/js/admin/service/service.js b/src/main/webapp/alfio-admin-v1/js/admin/service/service.js index e60899014e..5989bc498b 100644 --- a/src/main/webapp/alfio-admin-v1/js/admin/service/service.js +++ b/src/main/webapp/alfio-admin-v1/js/admin/service/service.js @@ -1,6 +1,6 @@ (function () { "use strict"; - var baseServices = angular.module('adminServices', ['angular-growl' , 'ngAnimate']); + var baseServices = angular.module('adminServices', ['ngAnimate']); baseServices.config(['$httpProvider', function($httpProvider) { var token = $("meta[name='_csrf']").attr("content"); @@ -660,26 +660,47 @@ }; }]); - baseServices.service("NotificationHandler", ["growl", "$sanitize", function (growl, $sanitize) { - var config = {ttl: 5000, disableCountDown: true}; - var sanitize = function(message) { - var sanitized = $sanitize(message); - return sanitized.split(' ').map(function(part) { - return encodeURIComponent(part); - }).join(' '); - }; + baseServices.service("NotificationHandler", [function () { return { showSuccess: function (message) { - return growl.success(sanitize(message), config); + window.dispatchEvent(new CustomEvent('alfio-feedback', { + detail: { + type: 'success', + message: message + }, + bubbles: true, + composed: true + })); }, showWarning: function (message) { - return growl.warning(sanitize(message), config); + window.dispatchEvent(new CustomEvent('alfio-feedback', { + detail: { + type: 'warning', + message: message + }, + bubbles: true, + composed: true + })); }, showInfo : function (message) { - return growl.info(sanitize(message), config); + window.dispatchEvent(new CustomEvent('alfio-feedback', { + detail: { + type: 'neutral', + message: message + }, + bubbles: true, + composed: true + })); }, showError : function (message, prefix) { - return growl.error((prefix || '') + sanitize(message), config); + window.dispatchEvent(new CustomEvent('alfio-feedback', { + detail: { + type: 'danger', + message: message + }, + bubbles: true, + composed: true + })); } } diff --git a/src/test/java/alfio/controller/api/v2/user/reservation/BaseReservationFlowTest.java b/src/test/java/alfio/controller/api/v2/user/reservation/BaseReservationFlowTest.java index ca6729e16d..ab930bbf81 100644 --- a/src/test/java/alfio/controller/api/v2/user/reservation/BaseReservationFlowTest.java +++ b/src/test/java/alfio/controller/api/v2/user/reservation/BaseReservationFlowTest.java @@ -215,7 +215,9 @@ private EventModification.AdditionalService buildAdditionalService(ReservationFl Collections.singletonList(new EventModification.AdditionalServiceText(null, "en", "additional desc", AdditionalServiceText.TextType.DESCRIPTION)), AdditionalService.AdditionalServiceType.SUPPLEMENT, - supplementPolicy + supplementPolicy, + null, + null ); } diff --git a/src/test/java/alfio/manager/AdditionalServiceManagerTest.java b/src/test/java/alfio/manager/AdditionalServiceManagerTest.java index 3f4421fdd7..9a3c2560d9 100644 --- a/src/test/java/alfio/manager/AdditionalServiceManagerTest.java +++ b/src/test/java/alfio/manager/AdditionalServiceManagerTest.java @@ -16,6 +16,7 @@ */ package alfio.manager; +import alfio.manager.support.reservation.ReservationCostCalculator; import alfio.model.AdditionalService; import alfio.model.Event; import alfio.model.PromoCodeDiscount; @@ -48,6 +49,7 @@ public class AdditionalServiceManagerTest { private NamedParameterJdbcTemplate jdbcTemplate; private TicketRepository ticketRepository; private PurchaseContextFieldRepository purchaseContextFieldRepository; + private ReservationCostCalculator reservationCostCalculator; @BeforeEach void init() { @@ -61,6 +63,7 @@ void init() { jdbcTemplate = mock(NamedParameterJdbcTemplate.class); purchaseContextFieldRepository = mock(PurchaseContextFieldRepository.class); ticketRepository = mock(TicketRepository.class); + reservationCostCalculator = mock(ReservationCostCalculator.class); } @ParameterizedTest @@ -73,11 +76,11 @@ public void testNonFixedPriceCommited(int availQuantity) { final PromoCodeDiscount DISCOUNT = null; final String RESERVATION_ID = "7ddadb25-18e8-4c72-9727-11f0bdfdb698"; final int REQUESTED_QUANTITY = 1; - final BigDecimal AMOUNT = new BigDecimal(5.74); + final BigDecimal AMOUNT = new BigDecimal("5.74"); final boolean IS_FIXED_PRICE = false; - when(additionalService.getAvailableQuantity()).thenReturn(availQuantity); - when(additionalService.isFixPrice()).thenReturn(IS_FIXED_PRICE); + when(additionalService.availableQuantity()).thenReturn(availQuantity); + when(additionalService.fixPrice()).thenReturn(IS_FIXED_PRICE); when(jdbcTemplate.batchUpdate(any(), any(SqlParameterSource[].class))).thenReturn(new int[]{1}); final ArrayList FREE_SERVICE_ITEMS = new ArrayList<>(); @@ -90,7 +93,8 @@ public void testNonFixedPriceCommited(int availQuantity) { additionalServiceItemRepository, jdbcTemplate, ticketRepository, - purchaseContextFieldRepository + purchaseContextFieldRepository, + reservationCostCalculator ); additionalServiceManager.bookAdditionalServiceItems( diff --git a/src/test/java/alfio/manager/CheckInManagerIntegrationTest.java b/src/test/java/alfio/manager/CheckInManagerIntegrationTest.java index b7d7ca8366..2deae06439 100644 --- a/src/test/java/alfio/manager/CheckInManagerIntegrationTest.java +++ b/src/test/java/alfio/manager/CheckInManagerIntegrationTest.java @@ -113,7 +113,7 @@ void testReturnOnlyOnce() { List.of(new EventModification.AdditionalServiceText(null, "en", "bla", AdditionalServiceText.TextType.TITLE)), List.of(new EventModification.AdditionalServiceText(null, "en", "blabla", AdditionalServiceText.TextType.DESCRIPTION)), AdditionalService.AdditionalServiceType.SUPPLEMENT, - AdditionalService.SupplementPolicy.OPTIONAL_UNLIMITED_AMOUNT + AdditionalService.SupplementPolicy.OPTIONAL_UNLIMITED_AMOUNT, null, null ); var additionalService = additionalServiceManager.insertAdditionalService(event, additionalServiceRequest); var category = ticketCategoryRepository.findAllTicketCategories(event.getId()).get(0); diff --git a/src/test/java/alfio/manager/PercentageAdditionalServicesIntegrationTest.java b/src/test/java/alfio/manager/PercentageAdditionalServicesIntegrationTest.java new file mode 100644 index 0000000000..bc997eaab0 --- /dev/null +++ b/src/test/java/alfio/manager/PercentageAdditionalServicesIntegrationTest.java @@ -0,0 +1,242 @@ +/** + * This file is part of alf.io. + * + * alf.io is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * alf.io is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with alf.io. If not, see . + */ +package alfio.manager; + +import alfio.TestConfiguration; +import alfio.config.DataSourceConfiguration; +import alfio.config.Initializer; +import alfio.manager.support.reservation.ReservationCostCalculator; +import alfio.manager.user.UserManager; +import alfio.model.*; +import alfio.model.metadata.AlfioMetadata; +import alfio.model.modification.*; +import alfio.repository.EventRepository; +import alfio.repository.TicketCategoryRepository; +import alfio.repository.system.ConfigurationRepository; +import alfio.repository.user.OrganizationRepository; +import alfio.test.util.AlfioIntegrationTest; +import alfio.test.util.IntegrationTestUtil; +import alfio.util.ClockProvider; +import org.apache.commons.lang3.time.DateUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZonedDateTime; +import java.util.*; + +import static alfio.test.util.IntegrationTestUtil.*; +import static alfio.util.MonetaryUtil.HUNDRED; + +@AlfioIntegrationTest +@ContextConfiguration(classes = {DataSourceConfiguration.class, TestConfiguration.class}) +@ActiveProfiles({Initializer.PROFILE_DEV, Initializer.PROFILE_DISABLE_JOBS, Initializer.PROFILE_INTEGRATION_TEST}) +public class PercentageAdditionalServicesIntegrationTest { + @Autowired + private ReservationCostCalculator reservationCostCalculator; + @Autowired + private TicketReservationManager reservationManager; + @Autowired + private NamedParameterJdbcTemplate jdbcTemplate; + @Autowired + private ConfigurationRepository configurationRepository; + @Autowired + private OrganizationRepository organizationRepository; + @Autowired + private UserManager userManager; + @Autowired + private EventManager eventManager; + @Autowired + private EventRepository eventRepository; + @Autowired + private TicketCategoryRepository ticketCategoryRepository; + + @BeforeEach + void setUp() { + IntegrationTestUtil.ensureMinimalConfiguration(configurationRepository); + } + + @Nested + class PercentageOnReservation { + @Test + void taxIncluded() { + // percentage fee with no min/max + var event = initData(BigDecimal.TEN, PriceContainer.VatStatus.INCLUDED, AdditionalService.SupplementPolicy.MANDATORY_PERCENTAGE_RESERVATION, null, null); + var totalPrice = bookAndCalculatePrice(event); + // we expect a price of 100.00 for the ticket + 100.00 for the additional service + 10% + VAT = 220.00 + Assertions.assertEquals(22000, totalPrice.getPriceWithVAT()); + } + + @Test + void taxIncludedMinPrice() { + // percentage fee with no min/max + var event = initData(BigDecimal.TEN, PriceContainer.VatStatus.INCLUDED, AdditionalService.SupplementPolicy.MANDATORY_PERCENTAGE_RESERVATION, new BigDecimal("20.01"), null); + var totalPrice = bookAndCalculatePrice(event); + Assertions.assertEquals(22001, totalPrice.getPriceWithVAT()); + } + + @Test + void taxIncludedMaxPrice() { + // percentage fee with no min/max + var event = initData(BigDecimal.TEN, PriceContainer.VatStatus.INCLUDED, AdditionalService.SupplementPolicy.MANDATORY_PERCENTAGE_RESERVATION, null, new BigDecimal("9.99")); + var totalPrice = bookAndCalculatePrice(event); + Assertions.assertEquals(20999, totalPrice.getPriceWithVAT()); + } + + @Test + void taxNotIncluded() { + // percentage fee with no min/max + var event = initData(BigDecimal.TEN, PriceContainer.VatStatus.NOT_INCLUDED, AdditionalService.SupplementPolicy.MANDATORY_PERCENTAGE_RESERVATION, null, null); + var totalPrice = bookAndCalculatePrice(event); + // we expect a price of 100.00 for the ticket + additional item + 10% + VAT = 222.20 + Assertions.assertEquals(22220, totalPrice.getPriceWithVAT()); + } + + } + + @Nested + class PercentageOnTickets { + + @Test + void taxIncluded() { + // percentage fee with no min/max + var event = initData(BigDecimal.TEN, PriceContainer.VatStatus.INCLUDED, AdditionalService.SupplementPolicy.MANDATORY_PERCENTAGE_FOR_TICKET, null, null); + + var totalPrice = bookAndCalculatePrice(event); + // we expect a price of 100.00 for the ticket + 10% + VAT = 110.00 + Assertions.assertEquals(21000, totalPrice.getPriceWithVAT()); + } + + @Test + void taxIncludedDecimalPercentage() { + // percentage fee with no min/max + var event = initData(new BigDecimal("10.5"), PriceContainer.VatStatus.INCLUDED, AdditionalService.SupplementPolicy.MANDATORY_PERCENTAGE_FOR_TICKET, null, null); + + var totalPrice = bookAndCalculatePrice(event); + // we expect a price of 100.00 for the ticket + 10% + VAT = 110.00 + Assertions.assertEquals(21100, totalPrice.getPriceWithVAT()); + } + + @Test + void taxIncludedMinPrice() { + // percentage fee with no min/max + var event = initData(BigDecimal.TEN, PriceContainer.VatStatus.INCLUDED, AdditionalService.SupplementPolicy.MANDATORY_PERCENTAGE_FOR_TICKET, new BigDecimal("10.01"), null); + var totalPrice = bookAndCalculatePrice(event); + // we expect a price of 100.00 for the ticket + 10.01 = 110.01 + Assertions.assertEquals(21001, totalPrice.getPriceWithVAT()); + } + + @Test + void taxIncludedMaxPrice() { + // percentage fee with no min/max + var event = initData(BigDecimal.TEN, PriceContainer.VatStatus.INCLUDED, AdditionalService.SupplementPolicy.MANDATORY_PERCENTAGE_FOR_TICKET, null, new BigDecimal("9.99")); + var totalPrice = bookAndCalculatePrice(event); + // we expect a price of 100.00 for the ticket + 10.01 = 110.01 + Assertions.assertEquals(20999, totalPrice.getPriceWithVAT()); + } + + @Test + void taxNotIncluded() { + // percentage fee with no min/max + var event = initData(BigDecimal.TEN, PriceContainer.VatStatus.NOT_INCLUDED, AdditionalService.SupplementPolicy.MANDATORY_PERCENTAGE_FOR_TICKET, null, null); + var totalPrice = bookAndCalculatePrice(event); + // we expect a price of 100.00 for the ticket + 10% + VAT = 111.00 + Assertions.assertEquals(21210, totalPrice.getPriceWithVAT()); + } + } + + + private Event initData(BigDecimal price, + PriceContainer.VatStatus eventVatStatus, + AdditionalService.SupplementPolicy supplementPolicy, + BigDecimal minPrice, + BigDecimal maxPrice) { + + var mandatory = new EventModification.AdditionalService( + null, + price, + false, + 1, + -1, + -1, + DateTimeModification.fromZonedDateTime(ZonedDateTime.now(ClockProvider.clock()).minusDays(1)), + DateTimeModification.fromZonedDateTime(ZonedDateTime.now(ClockProvider.clock()).plusDays(1)), + BigDecimal.ZERO, + AdditionalService.VatType.INHERITED, + List.of(), + List.of(new EventModification.AdditionalServiceText(null, "en", "mandatory", AdditionalServiceText.TextType.TITLE)), + List.of(new EventModification.AdditionalServiceText(null, "en", "mandatory description", AdditionalServiceText.TextType.DESCRIPTION)), + AdditionalService.AdditionalServiceType.SUPPLEMENT, + supplementPolicy, minPrice, maxPrice + ); + var optional = new EventModification.AdditionalService( + null, + new BigDecimal("100.00"), + true, + 2, + -1, + -1, + DateTimeModification.fromZonedDateTime(ZonedDateTime.now(ClockProvider.clock()).minusDays(1)), + DateTimeModification.fromZonedDateTime(ZonedDateTime.now(ClockProvider.clock()).plusDays(1)), + BigDecimal.ZERO, + AdditionalService.VatType.INHERITED, + List.of(), + List.of(new EventModification.AdditionalServiceText(null, "en", "optional", AdditionalServiceText.TextType.TITLE)), + List.of(new EventModification.AdditionalServiceText(null, "en", "optional description", AdditionalServiceText.TextType.DESCRIPTION)), + AdditionalService.AdditionalServiceType.SUPPLEMENT, + AdditionalService.SupplementPolicy.OPTIONAL_UNLIMITED_AMOUNT, null, null + ); + List categories = List.of( + new TicketCategoryModification(null, "default", TicketCategory.TicketAccessType.INHERIT, AVAILABLE_SEATS, + new DateTimeModification(LocalDate.now(ClockProvider.clock()).minusDays(1), LocalTime.now(ClockProvider.clock())), + new DateTimeModification(LocalDate.now(ClockProvider.clock()).plusDays(1), LocalTime.now(ClockProvider.clock())), + DESCRIPTION, HUNDRED, false, + "", false, null, null, null, null, null, 0, null, null, AlfioMetadata.empty()) + ); + Pair eventAndUser = initEvent(categories, organizationRepository, userManager, eventManager, eventRepository, List.of(mandatory, optional), Event.EventFormat.IN_PERSON, eventVatStatus); + + return eventAndUser.getLeft(); + } + + private TotalPrice bookAndCalculatePrice(Event event) { + var category = ticketCategoryRepository.findAllTicketCategories(event.getId()).get(0); + TicketReservationModification tr = new TicketReservationModification(); + tr.setQuantity(1); + tr.setTicketCategoryId(category.getId()); + var paramSource = new MapSqlParameterSource("eventId", event.getId()).addValue("policy", AdditionalService.SupplementPolicy.OPTIONAL_UNLIMITED_AMOUNT.name()); + int additionalServiceId = Objects.requireNonNull(jdbcTemplate.queryForObject("select id from additional_service where event_id_fk = :eventId and supplement_policy = :policy", paramSource, Integer.class)); + var additionalServices = new AdditionalServiceReservationModification(); + additionalServices.setAdditionalServiceId(additionalServiceId); + additionalServices.setQuantity(1); + + + var tickets = new TicketReservationWithOptionalCodeModification(tr, Optional.empty()); + String reservationId = reservationManager.createTicketReservation(event, List.of(tickets), List.of(new ASReservationWithOptionalCodeModification(additionalServices, Optional.empty())), DateUtils.addDays(new Date(), 1), Optional.empty(), Locale.ENGLISH, false, null); + Pair> priceAndDiscount = reservationCostCalculator.totalReservationCostWithVAT(reservationId); + return priceAndDiscount.getLeft(); + } +} diff --git a/src/test/java/alfio/manager/TicketReservationManagerIntegrationTest.java b/src/test/java/alfio/manager/TicketReservationManagerIntegrationTest.java index fea053d796..916715ffe2 100644 --- a/src/test/java/alfio/manager/TicketReservationManagerIntegrationTest.java +++ b/src/test/java/alfio/manager/TicketReservationManagerIntegrationTest.java @@ -380,8 +380,8 @@ public void testAdditionalServiceWithDiscount() { DESCRIPTION, BigDecimal.TEN, false, "", false, null, null, null, null, null, 0, null, null, AlfioMetadata.empty())); Event event = initEvent(categories, organizationRepository, userManager, eventManager, eventRepository).getKey(); - var firstAsKey = additionalServiceRepository.insert(event.getId(), 1000, true, 1, -1, 1, ZonedDateTime.now(ClockProvider.clock()).minusHours(1), ZonedDateTime.now(ClockProvider.clock()).plusHours(1), BigDecimal.TEN, AdditionalService.VatType.INHERITED, AdditionalService.AdditionalServiceType.SUPPLEMENT, AdditionalService.SupplementPolicy.OPTIONAL_UNLIMITED_AMOUNT); - var secondAsKey = additionalServiceRepository.insert(event.getId(), 500, true, 2, -1, 1, ZonedDateTime.now(ClockProvider.clock()).minusHours(1), ZonedDateTime.now(ClockProvider.clock()).plusHours(1), BigDecimal.TEN, AdditionalService.VatType.INHERITED, AdditionalService.AdditionalServiceType.DONATION, AdditionalService.SupplementPolicy.OPTIONAL_UNLIMITED_AMOUNT); + var firstAsKey = additionalServiceRepository.insert(event.getId(), 1000, true, 1, -1, 1, ZonedDateTime.now(ClockProvider.clock()).minusHours(1), ZonedDateTime.now(ClockProvider.clock()).plusHours(1), BigDecimal.TEN, AdditionalService.VatType.INHERITED, AdditionalService.AdditionalServiceType.SUPPLEMENT, AdditionalService.SupplementPolicy.OPTIONAL_UNLIMITED_AMOUNT, null, null); + var secondAsKey = additionalServiceRepository.insert(event.getId(), 500, true, 2, -1, 1, ZonedDateTime.now(ClockProvider.clock()).minusHours(1), ZonedDateTime.now(ClockProvider.clock()).plusHours(1), BigDecimal.TEN, AdditionalService.VatType.INHERITED, AdditionalService.AdditionalServiceType.DONATION, AdditionalService.SupplementPolicy.OPTIONAL_UNLIMITED_AMOUNT, null, null); TicketCategory unbounded = ticketCategoryRepository.findAllTicketCategories(event.getId()).stream().filter(t -> !t.isBounded()).findFirst().orElseThrow(IllegalStateException::new); @@ -492,7 +492,7 @@ public void testAccessCodeReleaseTickets() { } @Test - public void testWithAdditionalServices() { + void testWithAdditionalServices() { List categories = Collections.singletonList( new TicketCategoryModification(null, "default", TicketCategory.TicketAccessType.INHERIT, AVAILABLE_SEATS, new DateTimeModification(LocalDate.now(ClockProvider.clock()), LocalTime.now(ClockProvider.clock())), @@ -501,7 +501,7 @@ public void testWithAdditionalServices() { List additionalServices = Collections.singletonList(new EventModification.AdditionalService(null, BigDecimal.TEN, true, 1, 100, 5, DateTimeModification.fromZonedDateTime(ZonedDateTime.now(ClockProvider.clock()).minusDays(1L)), DateTimeModification.fromZonedDateTime(ZonedDateTime.now(ClockProvider.clock()).plusDays(1L)), - BigDecimal.TEN, AdditionalService.VatType.INHERITED, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), AdditionalService.AdditionalServiceType.SUPPLEMENT, AdditionalService.SupplementPolicy.OPTIONAL_UNLIMITED_AMOUNT)); + BigDecimal.TEN, AdditionalService.VatType.INHERITED, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), AdditionalService.AdditionalServiceType.SUPPLEMENT, AdditionalService.SupplementPolicy.OPTIONAL_UNLIMITED_AMOUNT, null, null)); Event event = initEvent(categories, organizationRepository, userManager, eventManager, eventRepository, additionalServices, Event.EventFormat.IN_PERSON).getKey(); TicketCategory unbounded = ticketCategoryRepository.findAllTicketCategories(event.getId()).stream().filter(t -> !t.isBounded()).findFirst().orElseThrow(IllegalStateException::new); @@ -512,7 +512,7 @@ public void testWithAdditionalServices() { TicketReservationWithOptionalCodeModification mod = new TicketReservationWithOptionalCodeModification(tr, Optional.empty()); AdditionalServiceReservationModification asrm = new AdditionalServiceReservationModification(); - asrm.setAdditionalServiceId(additionalServiceRepository.loadAllForEvent(event.getId()).get(0).getId()); + asrm.setAdditionalServiceId(additionalServiceRepository.loadAllForEvent(event.getId()).get(0).id()); asrm.setQuantity(1); ASReservationWithOptionalCodeModification asMod = new ASReservationWithOptionalCodeModification(asrm, Optional.empty()); @@ -532,7 +532,7 @@ public void testWithAdditionalServices() { assertEquals("40.00", orderSummary.getTotalPrice()); assertEquals("0.40", orderSummary.getTotalVAT()); assertEquals(3, orderSummary.getTicketAmount()); - List asRows = orderSummary.getSummary().stream().filter(s -> s.getType() == SummaryRow.SummaryType.ADDITIONAL_SERVICE).collect(Collectors.toList()); + List asRows = orderSummary.getSummary().stream().filter(s -> s.getType() == SummaryRow.SummaryType.ADDITIONAL_SERVICE).toList(); assertEquals(1, asRows.size()); assertEquals("9.90", asRows.get(0).getPriceBeforeVat()); assertEquals("9.90", asRows.get(0).getSubTotalBeforeVat()); @@ -540,7 +540,7 @@ public void testWithAdditionalServices() { } @Test - public void testTicketSelectionNotEnoughTicketsAvailable() { + void testTicketSelectionNotEnoughTicketsAvailable() { List categories = Collections.singletonList( new TicketCategoryModification(null, "default", TicketCategory.TicketAccessType.INHERIT, AVAILABLE_SEATS, new DateTimeModification(LocalDate.now(ClockProvider.clock()), LocalTime.now(ClockProvider.clock())), diff --git a/src/test/java/alfio/manager/TicketReservationManagerTest.java b/src/test/java/alfio/manager/TicketReservationManagerTest.java index c1cf8230b1..8dadd2df6e 100644 --- a/src/test/java/alfio/manager/TicketReservationManagerTest.java +++ b/src/test/java/alfio/manager/TicketReservationManagerTest.java @@ -222,7 +222,7 @@ void init() { reservationHelper = mock(ReservationEmailContentHelper.class); reservationCostCalculator = mock(ReservationCostCalculator.class); var osm = mock(OrderSummaryGenerator.class); - var additionalServiceManager = new AdditionalServiceManager(additionalServiceRepository, additionalServiceTextRepository, additionalServiceItemRepository, mock(NamedParameterJdbcTemplate.class), mock(TicketRepository.class), purchaseContextFieldRepository); + var additionalServiceManager = new AdditionalServiceManager(additionalServiceRepository, additionalServiceTextRepository, additionalServiceItemRepository, mock(NamedParameterJdbcTemplate.class), mock(TicketRepository.class), purchaseContextFieldRepository, reservationCostCalculator); reservationFinalizer = new ReservationFinalizer(transactionManager, ticketReservationRepository, userRepository, extensionManager, auditingRepository, TestUtil.clockProvider(), configurationManager, null, ticketRepository, reservationHelper, specialPriceRepository, diff --git a/src/test/java/alfio/manager/TicketReservationManagerUnitTest.java b/src/test/java/alfio/manager/TicketReservationManagerUnitTest.java index d820bdd296..525cd69208 100644 --- a/src/test/java/alfio/manager/TicketReservationManagerUnitTest.java +++ b/src/test/java/alfio/manager/TicketReservationManagerUnitTest.java @@ -124,7 +124,7 @@ public void setUp() { groupManager = mock(GroupManager.class); BillingDocumentRepository billingDocumentRepository = mock(BillingDocumentRepository.class); json = mock(Json.class); - var additionalServiceManager = new AdditionalServiceManager(additionalServiceRepository, additionalServiceTextRepository, additionalServiceItemRepository, mock(NamedParameterJdbcTemplate.class), mock(TicketRepository.class), purchaseContextFieldRepository); + var additionalServiceManager = new AdditionalServiceManager(additionalServiceRepository, additionalServiceTextRepository, additionalServiceItemRepository, mock(NamedParameterJdbcTemplate.class), mock(TicketRepository.class), purchaseContextFieldRepository, reservationCostCalculator); when(messageSourceManager.getMessageSourceFor(any())).thenReturn(messageSource); when(messageSourceManager.getRootMessageSource()).thenReturn(messageSource); @@ -286,15 +286,15 @@ private void initReservationWithAdditionalServices(boolean eventVatIncluded, Add AdditionalServiceItem additionalServiceItem = mock(AdditionalServiceItem.class); when(additionalServiceItem.getCurrencyCode()).thenReturn("CHF"); AdditionalService additionalService = mock(AdditionalService.class); - when(additionalService.getCurrencyCode()).thenReturn("CHF"); - when(additionalService.getId()).thenReturn(1); + when(additionalService.currencyCode()).thenReturn("CHF"); + when(additionalService.id()).thenReturn(1); when(additionalServiceItemRepository.findByReservationUuid(anyInt(), eq(TICKET_RESERVATION_ID))).thenReturn(Collections.singletonList(additionalServiceItem)); when(additionalServiceItem.getAdditionalServiceId()).thenReturn(1); when(additionalServiceRepository.loadAllForEvent(eq(1))).thenReturn(List.of(additionalService)); when(additionalServiceRepository.getById(eq(1), eq(1))).thenReturn(additionalService); when(additionalServiceItem.getSrcPriceCts()).thenReturn(asSrcPrice); - when(additionalService.getVatType()).thenReturn(additionalServiceVatType); + when(additionalService.vatType()).thenReturn(additionalServiceVatType); AdditionalServiceItemRepository additionalServiceItemRepository = mock(AdditionalServiceItemRepository.class); when(additionalServiceItemRepository.findByReservationUuid(anyInt(), eq(TICKET_RESERVATION_ID))).thenReturn(Collections.emptyList()); AdditionalServiceText text = mock(AdditionalServiceText.class); diff --git a/src/test/java/alfio/manager/support/reservation/ReservationCostCalculatorTest.java b/src/test/java/alfio/manager/support/reservation/ReservationCostCalculatorTest.java index f6432c0c7b..08aedb2353 100644 --- a/src/test/java/alfio/manager/support/reservation/ReservationCostCalculatorTest.java +++ b/src/test/java/alfio/manager/support/reservation/ReservationCostCalculatorTest.java @@ -173,15 +173,15 @@ private void initReservationWithAdditionalServices(boolean eventVatIncluded, Add AdditionalServiceItem additionalServiceItem = mock(AdditionalServiceItem.class); when(additionalServiceItem.getCurrencyCode()).thenReturn("CHF"); AdditionalService additionalService = mock(AdditionalService.class); - when(additionalService.getCurrencyCode()).thenReturn("CHF"); - when(additionalService.getId()).thenReturn(1); + when(additionalService.currencyCode()).thenReturn("CHF"); + when(additionalService.id()).thenReturn(1); when(additionalServiceItemRepository.findByReservationUuid(anyInt(), eq(TICKET_RESERVATION_ID))).thenReturn(Collections.singletonList(additionalServiceItem)); when(additionalServiceItem.getAdditionalServiceId()).thenReturn(1); when(additionalServiceRepository.loadAllForEvent(eq(1))).thenReturn(List.of(additionalService)); when(additionalServiceRepository.getById(eq(1), eq(1))).thenReturn(additionalService); when(additionalServiceItem.getSrcPriceCts()).thenReturn(asSrcPrice); - when(additionalService.getVatType()).thenReturn(additionalServiceVatType); + when(additionalService.vatType()).thenReturn(additionalServiceVatType); AdditionalServiceItemRepository additionalServiceItemRepository = mock(AdditionalServiceItemRepository.class); when(additionalServiceItemRepository.findByReservationUuid(anyInt(), eq(TICKET_RESERVATION_ID))).thenReturn(Collections.emptyList()); AdditionalServiceText text = mock(AdditionalServiceText.class); diff --git a/src/test/java/alfio/manager/system/ReservationPriceCalculatorTest.java b/src/test/java/alfio/manager/system/ReservationPriceCalculatorTest.java index e0e6d189c4..b0d4c18fe9 100644 --- a/src/test/java/alfio/manager/system/ReservationPriceCalculatorTest.java +++ b/src/test/java/alfio/manager/system/ReservationPriceCalculatorTest.java @@ -99,14 +99,14 @@ void init() { @Test void allItemsAreSubjectedToTaxation() { - when(additionalService.getVatType()).thenReturn(AdditionalService.VatType.INHERITED); + when(additionalService.vatType()).thenReturn(AdditionalService.VatType.INHERITED); var taxablePrice = new ReservationPriceCalculator(reservation, null, tickets, additionalServiceItems, additionalServices, event, List.of(), Optional.empty()).getTaxablePrice(); assertEquals(new BigDecimal("21.00"), taxablePrice); } @Test void additionalItemsAreNotSubjectedToTaxation() { - when(additionalService.getVatType()).thenReturn(AdditionalService.VatType.NONE); + when(additionalService.vatType()).thenReturn(AdditionalService.VatType.NONE); var taxablePrice = new ReservationPriceCalculator(reservation, null, tickets, additionalServiceItems, additionalServices, event, List.of(), Optional.empty()).getTaxablePrice(); assertEquals(new BigDecimal("10.00"), taxablePrice); } diff --git a/src/test/java/alfio/model/decorator/AdditionalServiceItemPriceContainerTest.java b/src/test/java/alfio/model/decorator/AdditionalServiceItemPriceContainerTest.java index 733935b5cb..d2ab59ea06 100644 --- a/src/test/java/alfio/model/decorator/AdditionalServiceItemPriceContainerTest.java +++ b/src/test/java/alfio/model/decorator/AdditionalServiceItemPriceContainerTest.java @@ -53,7 +53,7 @@ void fromWithDiscountCodeNull() { @Test void fromDonation() { - when(additionalService.getType()).thenReturn(AdditionalService.AdditionalServiceType.DONATION); + when(additionalService.type()).thenReturn(AdditionalService.AdditionalServiceType.DONATION); var priceContainer = AdditionalServiceItemPriceContainer.from(additionalServiceItem, additionalService, event, discount); assertNotNull(priceContainer); assertFalse(priceContainer.getDiscount().isPresent()); @@ -61,7 +61,7 @@ void fromDonation() { @Test void fromSupplementPercentageDiscount() { - when(additionalService.getType()).thenReturn(AdditionalService.AdditionalServiceType.SUPPLEMENT); + when(additionalService.type()).thenReturn(AdditionalService.AdditionalServiceType.SUPPLEMENT); when(discount.getDiscountType()).thenReturn(PromoCodeDiscount.DiscountType.PERCENTAGE); when(discount.getCodeType()).thenReturn(PromoCodeDiscount.CodeType.DISCOUNT); var priceContainer = AdditionalServiceItemPriceContainer.from(additionalServiceItem, additionalService, event, discount); @@ -72,7 +72,7 @@ void fromSupplementPercentageDiscount() { @Test void fromSupplement() { - when(additionalService.getType()).thenReturn(AdditionalService.AdditionalServiceType.SUPPLEMENT); + when(additionalService.type()).thenReturn(AdditionalService.AdditionalServiceType.SUPPLEMENT); when(discount.getDiscountType()).thenReturn(PromoCodeDiscount.DiscountType.FIXED_AMOUNT); when(discount.getCodeType()).thenReturn(PromoCodeDiscount.CodeType.DISCOUNT); var priceContainer = AdditionalServiceItemPriceContainer.from(additionalServiceItem, additionalService, event, discount); diff --git a/src/test/java/alfio/util/ValidatorTest.java b/src/test/java/alfio/util/ValidatorTest.java index fd7cb1e27b..8854af2586 100644 --- a/src/test/java/alfio/util/ValidatorTest.java +++ b/src/test/java/alfio/util/ValidatorTest.java @@ -107,65 +107,77 @@ void failedCategoryDescriptionValidation() { @Test void testValidationSuccess() { - EventModification.AdditionalService valid1 = new EventModification.AdditionalService(0, BigDecimal.ZERO, false, 0, -1, 1, VALID_INCEPTION, VALID_EXPIRATION, null, AdditionalService.VatType.NONE, Collections.emptyList(), singletonList(title), singletonList(description), AdditionalService.AdditionalServiceType.DONATION, null); - EventModification.AdditionalService valid2 = new EventModification.AdditionalService(0, BigDecimal.ONE, true, 1, 100, 1, VALID_INCEPTION, VALID_EXPIRATION, BigDecimal.TEN, AdditionalService.VatType.INHERITED, Collections.emptyList(), singletonList(title), singletonList(description), AdditionalService.AdditionalServiceType.DONATION, null); + EventModification.AdditionalService valid1 = new EventModification.AdditionalService(0, BigDecimal.ZERO, false, 0, -1, 1, VALID_INCEPTION, VALID_EXPIRATION, null, AdditionalService.VatType.NONE, Collections.emptyList(), singletonList(title), singletonList(description), AdditionalService.AdditionalServiceType.DONATION, null, null, null); + EventModification.AdditionalService valid2 = new EventModification.AdditionalService(0, BigDecimal.ONE, true, 1, 100, 1, VALID_INCEPTION, VALID_EXPIRATION, BigDecimal.TEN, AdditionalService.VatType.INHERITED, Collections.emptyList(), singletonList(title), singletonList(description), AdditionalService.AdditionalServiceType.DONATION, null, null, null); assertTrue(Stream.of(valid1, valid2).map(as -> Validator.validateAdditionalService(as, errors)).allMatch(ValidationResult::isSuccess)); assertFalse(errors.hasFieldErrors()); } @Test void testValidationErrorExpirationBeforeInception() { - EventModification.AdditionalService invalid = new EventModification.AdditionalService(0, BigDecimal.ZERO, false, 0, -1, 1, VALID_EXPIRATION, VALID_INCEPTION, null, AdditionalService.VatType.NONE, Collections.emptyList(), singletonList(title), singletonList(description), AdditionalService.AdditionalServiceType.DONATION, null); + EventModification.AdditionalService invalid = new EventModification.AdditionalService(0, BigDecimal.ZERO, false, 0, -1, 1, VALID_EXPIRATION, VALID_INCEPTION, null, AdditionalService.VatType.NONE, Collections.emptyList(), singletonList(title), singletonList(description), AdditionalService.AdditionalServiceType.DONATION, null, null, null); assertFalse(Validator.validateAdditionalService(invalid, errors).isSuccess()); assertTrue(errors.hasFieldErrors()); - assertNotNull(errors.getFieldError("additionalServices")); + assertEquals(2, errors.getErrorCount()); + assertNotNull(errors.getFieldError("inception")); + assertNotNull(errors.getFieldError("expiration")); } @Test void testValidationErrorInceptionNull() { - EventModification.AdditionalService invalid = new EventModification.AdditionalService(0, BigDecimal.ONE, true, 1, 100, 1, null, VALID_EXPIRATION, BigDecimal.TEN, AdditionalService.VatType.INHERITED, Collections.emptyList(), singletonList(title), singletonList(description), AdditionalService.AdditionalServiceType.DONATION, null); + EventModification.AdditionalService invalid = new EventModification.AdditionalService(0, BigDecimal.ONE, true, 1, 100, 1, null, VALID_EXPIRATION, BigDecimal.TEN, AdditionalService.VatType.INHERITED, Collections.emptyList(), singletonList(title), singletonList(description), AdditionalService.AdditionalServiceType.DONATION, null, null, null); assertFalse(Validator.validateAdditionalService(invalid, errors).isSuccess()); assertTrue(errors.hasFieldErrors()); - assertNotNull(errors.getFieldError("additionalServices")); + assertEquals(2, errors.getErrorCount()); + assertNotNull(errors.getFieldError("inception")); + assertNotNull(errors.getFieldError("expiration")); } @Test void testValidationExpirationNull() { - EventModification.AdditionalService invalid = new EventModification.AdditionalService(0, BigDecimal.ONE, true, 1, 100, 1, VALID_INCEPTION, null, BigDecimal.TEN, AdditionalService.VatType.INHERITED, Collections.emptyList(), singletonList(title), singletonList(description), AdditionalService.AdditionalServiceType.DONATION, null); + EventModification.AdditionalService invalid = new EventModification.AdditionalService(0, BigDecimal.ONE, true, 1, 100, 1, VALID_INCEPTION, null, BigDecimal.TEN, AdditionalService.VatType.INHERITED, Collections.emptyList(), singletonList(title), singletonList(description), AdditionalService.AdditionalServiceType.DONATION, null, null, null); assertFalse(Validator.validateAdditionalService(invalid, errors).isSuccess()); assertTrue(errors.hasFieldErrors()); - assertNotNull(errors.getFieldError("additionalServices")); + assertEquals(2, errors.getErrorCount()); + assertNotNull(errors.getFieldError("inception")); + assertNotNull(errors.getFieldError("expiration")); } @Test void testValidationInceptionExpirationNull() { - EventModification.AdditionalService invalid = new EventModification.AdditionalService(0, BigDecimal.ONE, true, 1, 100, 1, null, null, BigDecimal.TEN, AdditionalService.VatType.INHERITED, Collections.emptyList(), singletonList(title), singletonList(description), AdditionalService.AdditionalServiceType.DONATION, null); + EventModification.AdditionalService invalid = new EventModification.AdditionalService(0, BigDecimal.ONE, true, 1, 100, 1, null, null, BigDecimal.TEN, AdditionalService.VatType.INHERITED, Collections.emptyList(), singletonList(title), singletonList(description), AdditionalService.AdditionalServiceType.DONATION, null, null, null); assertFalse(Validator.validateAdditionalService(invalid, errors).isSuccess()); assertTrue(errors.hasFieldErrors()); - assertNotNull(errors.getFieldError("additionalServices")); + assertEquals(2, errors.getErrorCount()); + assertNotNull(errors.getFieldError("inception")); + assertNotNull(errors.getFieldError("expiration")); } @Test void testValidationFailedDescriptionsDontMatchTitles() { - EventModification.AdditionalService invalid = new EventModification.AdditionalService(0, BigDecimal.ZERO, false, 0, -1, 1, VALID_INCEPTION, VALID_EXPIRATION, null, AdditionalService.VatType.NONE, Collections.emptyList(), emptyList(), singletonList(description), AdditionalService.AdditionalServiceType.DONATION, null); - EventModification.AdditionalService valid = new EventModification.AdditionalService(0, BigDecimal.ONE, true, 1, 100, 1, VALID_INCEPTION, VALID_EXPIRATION, BigDecimal.TEN, AdditionalService.VatType.INHERITED, Collections.emptyList(), singletonList(title), singletonList(description), AdditionalService.AdditionalServiceType.DONATION, null); + EventModification.AdditionalService invalid = new EventModification.AdditionalService(0, BigDecimal.ZERO, false, 0, -1, 1, VALID_INCEPTION, VALID_EXPIRATION, null, AdditionalService.VatType.NONE, Collections.emptyList(), emptyList(), singletonList(description), AdditionalService.AdditionalServiceType.DONATION, null, null, null); + EventModification.AdditionalService valid = new EventModification.AdditionalService(0, BigDecimal.ONE, true, 1, 100, 1, VALID_INCEPTION, VALID_EXPIRATION, BigDecimal.TEN, AdditionalService.VatType.INHERITED, Collections.emptyList(), singletonList(title), singletonList(description), AdditionalService.AdditionalServiceType.DONATION, null, null, null); assertTrue(Validator.validateAdditionalService(valid, errors).isSuccess()); assertFalse(Validator.validateAdditionalService(invalid, errors).isSuccess()); assertTrue(errors.hasFieldErrors()); - assertNotNull(errors.getFieldError("additionalServices")); + assertEquals(2, errors.getErrorCount()); + assertNotNull(errors.getFieldError("title")); + assertNotNull(errors.getFieldError("description")); } @Test void testValidationFailedDescription() { - EventModification.AdditionalService invalid1 = new EventModification.AdditionalService(0, BigDecimal.ZERO, false, 0, -1, 1, VALID_INCEPTION, VALID_EXPIRATION, null, AdditionalService.VatType.NONE, Collections.emptyList(), emptyList(), singletonList(description), AdditionalService.AdditionalServiceType.DONATION, null);//English is required here - EventModification.AdditionalService invalid2 = new EventModification.AdditionalService(0, BigDecimal.ONE, true, 1, 100, 1, VALID_INCEPTION, VALID_EXPIRATION, BigDecimal.TEN, AdditionalService.VatType.INHERITED, Collections.emptyList(), singletonList(title), singletonList(new EventModification.AdditionalServiceText(0, "en", "", AdditionalServiceText.TextType.DESCRIPTION)), AdditionalService.AdditionalServiceType.DONATION, null); + EventModification.AdditionalService invalid1 = new EventModification.AdditionalService(0, BigDecimal.ZERO, false, 0, -1, 1, VALID_INCEPTION, VALID_EXPIRATION, null, AdditionalService.VatType.NONE, Collections.emptyList(), emptyList(), singletonList(description), AdditionalService.AdditionalServiceType.DONATION, null, null, null);//English is required here + EventModification.AdditionalService invalid2 = new EventModification.AdditionalService(0, BigDecimal.ONE, true, 1, 100, 1, VALID_INCEPTION, VALID_EXPIRATION, BigDecimal.TEN, AdditionalService.VatType.INHERITED, Collections.emptyList(), singletonList(title), singletonList(new EventModification.AdditionalServiceText(0, "en", "", AdditionalServiceText.TextType.DESCRIPTION)), AdditionalService.AdditionalServiceType.DONATION, null, null, null); assertFalse(Validator.validateAdditionalService(invalid1, errors).isSuccess()); assertTrue(errors.hasFieldErrors()); - assertNotNull(errors.getFieldError("additionalServices")); + assertNotNull(errors.getFieldError("title")); + assertNotNull(errors.getFieldError("description")); assertFalse(Validator.validateAdditionalService(invalid2, errors).isSuccess()); assertTrue(errors.hasFieldErrors()); - assertNotNull(errors.getFieldError("additionalServices")); + assertNotNull(errors.getFieldError("title")); + assertNotNull(errors.getFieldError("description")); } @Test diff --git a/src/test/resources/api/descriptor.json b/src/test/resources/api/descriptor.json index 682c203351..4ef0d35ab2 100644 --- a/src/test/resources/api/descriptor.json +++ b/src/test/resources/api/descriptor.json @@ -26103,6 +26103,9 @@ "location" : { "type" : "string" }, + "code" : { + "type" : "string" + }, "description" : { "type" : "string" }, @@ -26111,9 +26114,6 @@ "items" : { "type" : "object" } - }, - "code" : { - "type" : "string" } } }, @@ -26497,6 +26497,13 @@ "refundedAmount" : { "type" : "string" }, + "ticketAmount" : { + "type" : "integer", + "format" : "int32" + }, + "singleTicketOrder" : { + "type" : "boolean" + }, "displayVat" : { "type" : "boolean" }, @@ -26512,13 +26519,6 @@ "priceBeforeTaxes" : { "type" : "string" }, - "singleTicketOrder" : { - "type" : "boolean" - }, - "ticketAmount" : { - "type" : "integer", - "format" : "int32" - }, "notYetPaid" : { "type" : "boolean" }, @@ -26968,11 +26968,11 @@ "type" : "string", "enum" : [ "INHERIT", "IN_PERSON", "ONLINE" ] }, - "free" : { - "type" : "boolean" - }, "price" : { "type" : "number" + }, + "free" : { + "type" : "boolean" } } }, @@ -27088,7 +27088,13 @@ "cancelled" : { "type" : "boolean" }, - "stuck" : { + "lineSplittedBillingAddress" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "hasInvoiceOrReceiptDocument" : { "type" : "boolean" }, "hasBillingAddress" : { @@ -27097,31 +27103,25 @@ "hasVatNumber" : { "type" : "boolean" }, + "paidAmount" : { + "type" : "string" + }, "hasInvoiceNumber" : { "type" : "boolean" }, "pendingOfflinePayment" : { "type" : "boolean" }, - "paidAmount" : { - "type" : "string" - }, - "lineSplittedBillingAddress" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "hasInvoiceOrReceiptDocument" : { + "hasBeenPaid" : { "type" : "boolean" }, - "hasBeenPaid" : { + "stuck" : { "type" : "boolean" }, - "finalPrice" : { + "netPrice" : { "type" : "number" }, - "netPrice" : { + "finalPrice" : { "type" : "number" }, "taxablePrice" : { @@ -27217,11 +27217,16 @@ "currencyCode" : { "type" : "string" }, - "status" : { - "type" : "string", - "enum" : [ "PENDING", "IN_PAYMENT", "EXTERNAL_PROCESSING_PAYMENT", "WAITING_EXTERNAL_CONFIRMATION", "OFFLINE_PAYMENT", "DEFERRED_OFFLINE_PAYMENT", "FINALIZING", "OFFLINE_FINALIZING", "COMPLETE", "STUCK", "CANCELLED", "CREDIT_NOTE_ISSUED" ] + "lineSplittedBillingAddress" : { + "type" : "array", + "items" : { + "type" : "string" + } }, - "stuck" : { + "directAssignmentRequested" : { + "type" : "boolean" + }, + "hasInvoiceOrReceiptDocument" : { "type" : "boolean" }, "reminderSent" : { @@ -27247,73 +27252,54 @@ "type" : "string", "format" : "date-time" }, - "customerReference" : { - "type" : "string" - }, - "hasInvoiceNumber" : { - "type" : "boolean" + "status" : { + "type" : "string", + "enum" : [ "PENDING", "IN_PAYMENT", "EXTERNAL_PROCESSING_PAYMENT", "WAITING_EXTERNAL_CONFIRMATION", "OFFLINE_PAYMENT", "DEFERRED_OFFLINE_PAYMENT", "FINALIZING", "OFFLINE_FINALIZING", "COMPLETE", "STUCK", "CANCELLED", "CREDIT_NOTE_ISSUED" ] }, - "invoiceNumber" : { - "type" : "string" + "vat" : { + "type" : "number" }, - "paymentMethod" : { - "type" : "string", - "enum" : [ "STRIPE", "ON_SITE", "OFFLINE", "NONE", "ADMIN", "PAYPAL", "MOLLIE", "SAFERPAY" ] + "vatCts" : { + "type" : "integer", + "format" : "int32" }, - "firstName" : { + "vatNr" : { "type" : "string" }, - "lastName" : { + "email" : { "type" : "string" }, "userLanguage" : { "type" : "string" }, - "srcPriceCts" : { - "type" : "integer", - "format" : "int32" - }, - "finalPrice" : { - "type" : "number" - }, - "appliedDiscount" : { - "type" : "number" - }, - "finalPriceCts" : { - "type" : "integer", - "format" : "int32" + "confirmationTimestamp" : { + "type" : "string", + "format" : "date-time" }, - "vatCts" : { - "type" : "integer", - "format" : "int32" + "paidAmount" : { + "type" : "string" }, - "discountCts" : { - "type" : "integer", - "format" : "int32" + "registrationTimestamp" : { + "type" : "string", + "format" : "date-time" }, - "vatNr" : { + "firstName" : { "type" : "string" }, - "vatCountryCode" : { + "lastName" : { "type" : "string" }, - "invoiceRequested" : { + "hasInvoiceNumber" : { "type" : "boolean" }, - "email" : { + "invoiceNumber" : { "type" : "string" }, - "vatStatus" : { + "paymentMethod" : { "type" : "string", - "enum" : [ "NONE", "INCLUDED", "NOT_INCLUDED", "INCLUDED_EXEMPT", "NOT_INCLUDED_EXEMPT", "CUSTOM_INCLUDED_EXEMPT", "CUSTOM_NOT_INCLUDED_EXEMPT", "INCLUDED_NOT_CHARGED", "NOT_INCLUDED_NOT_CHARGED" ] - }, - "vat" : { - "type" : "number" - }, - "discount" : { - "$ref" : "#/components/schemas/PromoCodeDiscount" + "enum" : [ "STRIPE", "ON_SITE", "OFFLINE", "NONE", "ADMIN", "PAYPAL", "MOLLIE", "SAFERPAY" ] }, - "pendingOfflinePayment" : { + "invoiceRequested" : { "type" : "boolean" }, "promoCodeDiscountId" : { @@ -27333,40 +27319,54 @@ "billingAddress" : { "type" : "string" }, - "registrationTimestamp" : { - "type" : "string", - "format" : "date-time" + "customerReference" : { + "type" : "string" }, - "confirmationTimestamp" : { + "srcPriceCts" : { + "type" : "integer", + "format" : "int32" + }, + "pendingOfflinePayment" : { + "type" : "boolean" + }, + "vatStatus" : { "type" : "string", - "format" : "date-time" + "enum" : [ "NONE", "INCLUDED", "NOT_INCLUDED", "INCLUDED_EXEMPT", "NOT_INCLUDED_EXEMPT", "CUSTOM_INCLUDED_EXEMPT", "CUSTOM_NOT_INCLUDED_EXEMPT", "INCLUDED_NOT_CHARGED", "NOT_INCLUDED_NOT_CHARGED" ] }, - "paidAmount" : { - "type" : "string" + "discount" : { + "$ref" : "#/components/schemas/PromoCodeDiscount" }, - "lineSplittedBillingAddress" : { - "type" : "array", - "items" : { - "type" : "string" - } + "finalPrice" : { + "type" : "number" }, - "directAssignmentRequested" : { - "type" : "boolean" + "appliedDiscount" : { + "type" : "number" }, - "vatIncluded" : { - "type" : "boolean" + "finalPriceCts" : { + "type" : "integer", + "format" : "int32" }, - "hasInvoiceOrReceiptDocument" : { - "type" : "boolean" + "discountCts" : { + "type" : "integer", + "format" : "int32" }, - "hasBeenPaid" : { - "type" : "boolean" + "vatCountryCode" : { + "type" : "string" }, "optionalVatPercentage" : { "type" : "number" }, "taxablePrice" : { "type" : "number" + }, + "vatIncluded" : { + "type" : "boolean" + }, + "hasBeenPaid" : { + "type" : "boolean" + }, + "stuck" : { + "type" : "boolean" } } }, @@ -27570,17 +27570,17 @@ "format" : "int32" } }, - "linkedCategoriesIds" : { + "restrictedValuesAsString" : { "type" : "array", "items" : { - "type" : "integer", - "format" : "int32" + "type" : "string" } }, - "restrictedValuesAsString" : { + "linkedCategoriesIds" : { "type" : "array", "items" : { - "type" : "string" + "type" : "integer", + "format" : "int32" } }, "disabledValuesAsString" : { @@ -27653,7 +27653,13 @@ }, "supplementPolicy" : { "type" : "string", - "enum" : [ "MANDATORY_ONE_FOR_TICKET", "OPTIONAL_UNLIMITED_AMOUNT", "OPTIONAL_MAX_AMOUNT_PER_TICKET", "OPTIONAL_MAX_AMOUNT_PER_RESERVATION" ] + "enum" : [ "MANDATORY_ONE_FOR_TICKET", "MANDATORY_PERCENTAGE_RESERVATION", "MANDATORY_PERCENTAGE_FOR_TICKET", "OPTIONAL_UNLIMITED_AMOUNT", "OPTIONAL_MAX_AMOUNT_PER_TICKET", "OPTIONAL_MAX_AMOUNT_PER_RESERVATION" ] + }, + "minPrice" : { + "type" : "number" + }, + "maxPrice" : { + "type" : "number" }, "finalPrice" : { "type" : "number" @@ -28170,11 +28176,14 @@ "errorMessage" : { "type" : "string" }, + "reservationStatusChanged" : { + "type" : "boolean" + }, "clientSecret" : { "type" : "string" }, - "reservationStatusChanged" : { - "type" : "boolean" + "token" : { + "type" : "string" }, "paymentMethod" : { "type" : "string", @@ -28183,9 +28192,6 @@ "paymentProvider" : { "type" : "string", "enum" : [ "STRIPE", "ON_SITE", "OFFLINE", "NONE", "ADMIN", "PAYPAL", "MOLLIE", "SAFERPAY" ] - }, - "token" : { - "type" : "string" } } }, @@ -28367,16 +28373,16 @@ "captcha" : { "type" : "string" }, - "additionalServices" : { + "tickets" : { "type" : "array", "items" : { - "$ref" : "#/components/schemas/AdditionalServiceReservationModification" + "$ref" : "#/components/schemas/TicketReservationModification" } }, - "tickets" : { + "additionalServices" : { "type" : "array", "items" : { - "$ref" : "#/components/schemas/TicketReservationModification" + "$ref" : "#/components/schemas/AdditionalServiceReservationModification" } } } @@ -28691,6 +28697,10 @@ "type" : "integer", "format" : "int32" }, + "numEntries" : { + "type" : "integer", + "format" : "int32" + }, "validityFrom" : { "type" : "string", "format" : "date-time" @@ -28702,10 +28712,6 @@ "timeUnit" : { "type" : "string", "enum" : [ "DAYS", "MONTHS", "YEARS" ] - }, - "numEntries" : { - "type" : "integer", - "format" : "int32" } } }, @@ -29263,65 +29269,66 @@ "currencyCode" : { "type" : "string" }, - "status" : { - "type" : "string", - "enum" : [ "FREE", "PENDING", "TO_BE_PAID", "ACQUIRED", "CANCELLED", "CHECKED_IN", "EXPIRED", "INVALIDATED", "RELEASED", "PRE_RESERVED" ] - }, "formattedNetPrice" : { "type" : "string" }, "extReference" : { "type" : "string" }, + "status" : { + "type" : "string", + "enum" : [ "FREE", "PENDING", "TO_BE_PAID", "ACQUIRED", "CANCELLED", "CHECKED_IN", "EXPIRED", "INVALIDATED", "RELEASED", "PRE_RESERVED" ] + }, "tags" : { "type" : "array", "items" : { "type" : "string" } }, - "assigned" : { - "type" : "boolean" - }, "eventId" : { "type" : "integer", "format" : "int32" }, - "firstName" : { + "vatCts" : { + "type" : "integer", + "format" : "int32" + }, + "email" : { "type" : "string" }, - "lastName" : { + "uuid" : { "type" : "string" }, + "assigned" : { + "type" : "boolean" + }, "userLanguage" : { "type" : "string" }, - "categoryId" : { - "type" : "integer", - "format" : "int32" - }, + "checkedIn" : { + "type" : "boolean" + }, + "firstName" : { + "type" : "string" + }, + "lastName" : { + "type" : "string" + }, "subscriptionId" : { "type" : "string", "format" : "uuid" }, - "srcPriceCts" : { - "type" : "integer", - "format" : "int32" - }, - "finalPriceCts" : { - "type" : "integer", - "format" : "int32" + "lockedAssignment" : { + "type" : "boolean" }, - "vatCts" : { + "srcPriceCts" : { "type" : "integer", "format" : "int32" }, - "discountCts" : { + "categoryId" : { "type" : "integer", "format" : "int32" }, - "email" : { - "type" : "string" - }, "ticketsReservationId" : { "type" : "string" }, @@ -29329,14 +29336,16 @@ "type" : "string", "enum" : [ "NONE", "INCLUDED", "NOT_INCLUDED", "INCLUDED_EXEMPT", "NOT_INCLUDED_EXEMPT", "CUSTOM_INCLUDED_EXEMPT", "CUSTOM_NOT_INCLUDED_EXEMPT", "INCLUDED_NOT_CHARGED", "NOT_INCLUDED_NOT_CHARGED" ] }, - "uuid" : { - "type" : "string" + "finalPriceCts" : { + "type" : "integer", + "format" : "int32" }, - "lockedAssignment" : { - "type" : "boolean" + "discountCts" : { + "type" : "integer", + "format" : "int32" }, - "checkedIn" : { - "type" : "boolean" + "categoryName" : { + "type" : "string" }, "creation" : { "type" : "string", @@ -29344,9 +29353,6 @@ }, "formattedFinalPrice" : { "type" : "string" - }, - "categoryName" : { - "type" : "string" } } }, @@ -29828,6 +29834,9 @@ } } }, + "overlap" : { + "type" : "boolean" + }, "duration" : { "type" : "object", "properties" : { @@ -29875,9 +29884,6 @@ "type" : "string", "format" : "date-time" }, - "overlap" : { - "type" : "boolean" - }, "instant" : { "type" : "string", "format" : "date-time" @@ -29958,12 +29964,11 @@ "supportsTicketsGeneration" : { "type" : "boolean" }, - "publicIdentifier" : { - "type" : "string" + "onSaleToModel" : { + "$ref" : "#/components/schemas/DateTimeModification" }, - "priceCts" : { - "type" : "integer", - "format" : "int32" + "onSaleFromModel" : { + "$ref" : "#/components/schemas/DateTimeModification" }, "validityFromModel" : { "$ref" : "#/components/schemas/DateTimeModification" @@ -29971,11 +29976,12 @@ "validityToModel" : { "$ref" : "#/components/schemas/DateTimeModification" }, - "onSaleFromModel" : { - "$ref" : "#/components/schemas/DateTimeModification" + "publicIdentifier" : { + "type" : "string" }, - "onSaleToModel" : { - "$ref" : "#/components/schemas/DateTimeModification" + "priceCts" : { + "type" : "integer", + "format" : "int32" } } }, @@ -30568,65 +30574,66 @@ "currencyCode" : { "type" : "string" }, - "status" : { - "type" : "string", - "enum" : [ "FREE", "PENDING", "TO_BE_PAID", "ACQUIRED", "CANCELLED", "CHECKED_IN", "EXPIRED", "INVALIDATED", "RELEASED", "PRE_RESERVED" ] - }, "formattedNetPrice" : { "type" : "string" }, "extReference" : { "type" : "string" }, + "status" : { + "type" : "string", + "enum" : [ "FREE", "PENDING", "TO_BE_PAID", "ACQUIRED", "CANCELLED", "CHECKED_IN", "EXPIRED", "INVALIDATED", "RELEASED", "PRE_RESERVED" ] + }, "tags" : { "type" : "array", "items" : { "type" : "string" } }, - "assigned" : { - "type" : "boolean" - }, "eventId" : { "type" : "integer", "format" : "int32" }, - "firstName" : { + "vatCts" : { + "type" : "integer", + "format" : "int32" + }, + "email" : { "type" : "string" }, - "lastName" : { + "uuid" : { "type" : "string" }, + "assigned" : { + "type" : "boolean" + }, "userLanguage" : { "type" : "string" }, - "categoryId" : { - "type" : "integer", - "format" : "int32" + "checkedIn" : { + "type" : "boolean" + }, + "firstName" : { + "type" : "string" + }, + "lastName" : { + "type" : "string" }, "subscriptionId" : { "type" : "string", "format" : "uuid" }, - "srcPriceCts" : { - "type" : "integer", - "format" : "int32" - }, - "finalPriceCts" : { - "type" : "integer", - "format" : "int32" + "lockedAssignment" : { + "type" : "boolean" }, - "vatCts" : { + "srcPriceCts" : { "type" : "integer", "format" : "int32" }, - "discountCts" : { + "categoryId" : { "type" : "integer", "format" : "int32" }, - "email" : { - "type" : "string" - }, "ticketsReservationId" : { "type" : "string" }, @@ -30634,14 +30641,13 @@ "type" : "string", "enum" : [ "NONE", "INCLUDED", "NOT_INCLUDED", "INCLUDED_EXEMPT", "NOT_INCLUDED_EXEMPT", "CUSTOM_INCLUDED_EXEMPT", "CUSTOM_NOT_INCLUDED_EXEMPT", "INCLUDED_NOT_CHARGED", "NOT_INCLUDED_NOT_CHARGED" ] }, - "uuid" : { - "type" : "string" - }, - "lockedAssignment" : { - "type" : "boolean" + "finalPriceCts" : { + "type" : "integer", + "format" : "int32" }, - "checkedIn" : { - "type" : "boolean" + "discountCts" : { + "type" : "integer", + "format" : "int32" }, "creation" : { "type" : "string", @@ -30917,6 +30923,9 @@ } } }, + "overlap" : { + "type" : "boolean" + }, "duration" : { "type" : "object", "properties" : { @@ -30964,9 +30973,6 @@ "type" : "string", "format" : "date-time" }, - "overlap" : { - "type" : "boolean" - }, "instant" : { "type" : "string", "format" : "date-time" @@ -31444,32 +31450,32 @@ "formattedPrice" : { "type" : "string" }, - "publicIdentifier" : { - "type" : "string" + "begin" : { + "type" : "string", + "format" : "date-time" }, - "allowedPaymentProxies" : { + "contentLanguages" : { "type" : "array", "items" : { - "type" : "string", - "enum" : [ "STRIPE", "ON_SITE", "OFFLINE", "NONE", "ADMIN", "PAYPAL", "MOLLIE", "SAFERPAY" ] + "$ref" : "#/components/schemas/ContentLanguage" } }, - "contentLanguages" : { + "privacyPolicyLinkOrNull" : { + "type" : "string" + }, + "allowedPaymentProxies" : { "type" : "array", "items" : { - "$ref" : "#/components/schemas/ContentLanguage" + "type" : "string", + "enum" : [ "STRIPE", "ON_SITE", "OFFLINE", "NONE", "ADMIN", "PAYPAL", "MOLLIE", "SAFERPAY" ] } }, - "begin" : { - "type" : "string", - "format" : "date-time" + "publicIdentifier" : { + "type" : "string" }, "freeOfCharge" : { "type" : "boolean" }, - "privacyPolicyLinkOrNull" : { - "type" : "string" - }, "fileBlobIdIsPresent" : { "type" : "boolean" }, @@ -31548,24 +31554,43 @@ "currency" : { "type" : "string" }, - "description" : { + "offlinePaymentConfiguration" : { + "$ref" : "#/components/schemas/OfflinePaymentConfiguration" + }, + "formattedPrice" : { + "type" : "string" + }, + "title" : { "type" : "object", "additionalProperties" : { "type" : "string" } }, - "formattedPrice" : { + "vat" : { "type" : "string" }, - "title" : { + "free" : { + "type" : "boolean" + }, + "description" : { "type" : "object", "additionalProperties" : { "type" : "string" } }, - "vat" : { + "assignmentConfiguration" : { + "$ref" : "#/components/schemas/AssignmentConfiguration" + }, + "canApplySubscriptions" : { + "type" : "boolean" + }, + "fileBlobId" : { "type" : "string" }, + "usageType" : { + "type" : "string", + "enum" : [ "ONCE_PER_EVENT", "UNLIMITED" ] + }, "termsAndConditionsUrl" : { "type" : "string" }, @@ -31587,37 +31612,18 @@ "vatIncluded" : { "type" : "boolean" }, - "usageType" : { - "type" : "string", - "enum" : [ "ONCE_PER_EVENT", "UNLIMITED" ] - }, - "free" : { - "type" : "boolean" - }, - "fileBlobId" : { - "type" : "string" - }, "validityType" : { "type" : "string", "enum" : [ "STANDARD", "CUSTOM", "NOT_SET" ] }, - "assignmentConfiguration" : { - "$ref" : "#/components/schemas/AssignmentConfiguration" - }, - "offlinePaymentConfiguration" : { - "$ref" : "#/components/schemas/OfflinePaymentConfiguration" - }, - "canApplySubscriptions" : { - "type" : "boolean" + "currencyDescriptor" : { + "$ref" : "#/components/schemas/CurrencyDescriptor" }, "contentLanguages" : { "type" : "array", "items" : { "$ref" : "#/components/schemas/Language" } - }, - "currencyDescriptor" : { - "$ref" : "#/components/schemas/CurrencyDescriptor" } } }, @@ -32001,13 +32007,13 @@ "$ref" : "#/components/schemas/AdditionalField" } }, - "fieldConfigurationAfterStandard" : { + "fieldConfigurationBeforeStandard" : { "type" : "array", "items" : { "$ref" : "#/components/schemas/AdditionalField" } }, - "fieldConfigurationBeforeStandard" : { + "fieldConfigurationAfterStandard" : { "type" : "array", "items" : { "$ref" : "#/components/schemas/AdditionalField" @@ -32053,20 +32059,20 @@ "redirectUrl" : { "type" : "string" }, + "failed" : { + "type" : "boolean" + }, "initialized" : { "type" : "boolean" }, "successful" : { "type" : "boolean" }, - "failed" : { - "type" : "boolean" + "gatewayIdOrNull" : { + "type" : "string" }, "redirect" : { "type" : "boolean" - }, - "gatewayIdOrNull" : { - "type" : "string" } } }, @@ -32257,11 +32263,11 @@ "type" : "string" } }, - "free" : { + "online" : { "type" : "boolean" }, - "publicIdentifier" : { - "type" : "string" + "free" : { + "type" : "boolean" }, "contentLanguages" : { "type" : "array", @@ -32269,27 +32275,12 @@ "$ref" : "#/components/schemas/ContentLanguage" } }, - "freeOfCharge" : { - "type" : "boolean" - }, - "regularPrice" : { - "type" : "number" - }, - "sameDay" : { - "type" : "boolean" - }, - "online" : { + "imageIsPresent" : { "type" : "boolean" }, - "privacyPolicyLinkOrNull" : { - "type" : "string" - }, "fileBlobIdIsPresent" : { "type" : "boolean" }, - "imageIsPresent" : { - "type" : "boolean" - }, "multiplePaymentMethods" : { "type" : "boolean" }, @@ -32300,6 +32291,9 @@ "useFirstAndLastName" : { "type" : "boolean" }, + "privacyPolicyLinkOrNull" : { + "type" : "string" + }, "beginTimeZoneOffset" : { "type" : "integer", "format" : "int32" @@ -32311,12 +32305,24 @@ "isOnline" : { "type" : "boolean" }, - "firstContentLanguage" : { - "$ref" : "#/components/schemas/ContentLanguage" - } - } - }, - "EventWithAdditionalInfo" : { + "publicIdentifier" : { + "type" : "string" + }, + "freeOfCharge" : { + "type" : "boolean" + }, + "regularPrice" : { + "type" : "number" + }, + "sameDay" : { + "type" : "boolean" + }, + "firstContentLanguage" : { + "$ref" : "#/components/schemas/ContentLanguage" + } + } + }, + "EventWithAdditionalInfo" : { "type" : "object", "properties" : { "event" : { @@ -32423,9 +32429,6 @@ "currency" : { "type" : "string" }, - "shortName" : { - "type" : "string" - }, "title" : { "type" : "object", "additionalProperties" : { @@ -32435,44 +32438,47 @@ "vat" : { "type" : "string" }, - "termsAndConditionsUrl" : { - "type" : "string" - }, - "privacyPolicyUrl" : { - "type" : "string" - }, - "vatIncluded" : { - "type" : "boolean" - }, "free" : { "type" : "boolean" }, - "fileBlobId" : { + "shortName" : { "type" : "string" }, "websiteUrl" : { "type" : "string" }, + "datesWithOffset" : { + "$ref" : "#/components/schemas/DatesWithTimeZoneOffset" + }, "organizationName" : { "type" : "string" }, "organizationEmail" : { "type" : "string" }, + "fileBlobId" : { + "type" : "string" + }, + "termsAndConditionsUrl" : { + "type" : "string" + }, + "privacyPolicyUrl" : { + "type" : "string" + }, + "vatIncluded" : { + "type" : "boolean" + }, "sameDay" : { "type" : "boolean" }, - "datesWithOffset" : { - "$ref" : "#/components/schemas/DatesWithTimeZoneOffset" + "currencyDescriptor" : { + "$ref" : "#/components/schemas/CurrencyDescriptor" }, "contentLanguages" : { "type" : "array", "items" : { "$ref" : "#/components/schemas/Language" } - }, - "currencyDescriptor" : { - "$ref" : "#/components/schemas/CurrencyDescriptor" } } }, @@ -33040,34 +33046,47 @@ "currencyCode" : { "type" : "string" }, - "status" : { - "type" : "string", - "enum" : [ "FREE", "PENDING", "TO_BE_PAID", "ACQUIRED", "CANCELLED", "CHECKED_IN", "EXPIRED", "INVALIDATED", "RELEASED", "PRE_RESERVED" ] - }, - "stuck" : { - "type" : "boolean" - }, "formattedNetPrice" : { "type" : "string" }, "extReference" : { "type" : "string" }, + "status" : { + "type" : "string", + "enum" : [ "FREE", "PENDING", "TO_BE_PAID", "ACQUIRED", "CANCELLED", "CHECKED_IN", "EXPIRED", "INVALIDATED", "RELEASED", "PRE_RESERVED" ] + }, "tags" : { "type" : "array", "items" : { "type" : "string" } }, + "eventId" : { + "type" : "integer", + "format" : "int32" + }, + "vatCts" : { + "type" : "integer", + "format" : "int32" + }, + "email" : { + "type" : "string" + }, + "uuid" : { + "type" : "string" + }, "assigned" : { "type" : "boolean" }, "transaction" : { "$ref" : "#/components/schemas/Transaction" }, - "eventId" : { - "type" : "integer", - "format" : "int32" + "userLanguage" : { + "type" : "string" + }, + "checkedIn" : { + "type" : "boolean" }, "firstName" : { "type" : "string" @@ -33075,39 +33094,25 @@ "lastName" : { "type" : "string" }, - "userLanguage" : { - "type" : "string" - }, - "categoryId" : { - "type" : "integer", - "format" : "int32" - }, "subscriptionId" : { "type" : "string", "format" : "uuid" }, - "srcPriceCts" : { - "type" : "integer", - "format" : "int32" - }, - "finalPrice" : { - "type" : "number" + "lockedAssignment" : { + "type" : "boolean" }, - "finalPriceCts" : { + "srcPriceCts" : { "type" : "integer", "format" : "int32" }, - "vatCts" : { - "type" : "integer", - "format" : "int32" + "transactionTimestamp" : { + "type" : "string", + "format" : "date-time" }, - "discountCts" : { + "categoryId" : { "type" : "integer", "format" : "int32" }, - "email" : { - "type" : "string" - }, "ticketsReservationId" : { "type" : "string" }, @@ -33115,18 +33120,16 @@ "type" : "string", "enum" : [ "NONE", "INCLUDED", "NOT_INCLUDED", "INCLUDED_EXEMPT", "NOT_INCLUDED_EXEMPT", "CUSTOM_INCLUDED_EXEMPT", "CUSTOM_NOT_INCLUDED_EXEMPT", "INCLUDED_NOT_CHARGED", "NOT_INCLUDED_NOT_CHARGED" ] }, - "transactionTimestamp" : { - "type" : "string", - "format" : "date-time" - }, - "uuid" : { - "type" : "string" + "finalPrice" : { + "type" : "number" }, - "lockedAssignment" : { - "type" : "boolean" + "finalPriceCts" : { + "type" : "integer", + "format" : "int32" }, - "checkedIn" : { - "type" : "boolean" + "discountCts" : { + "type" : "integer", + "format" : "int32" }, "creation" : { "type" : "string", @@ -33135,6 +33138,9 @@ "formattedFinalPrice" : { "type" : "string" }, + "stuck" : { + "type" : "boolean" + }, "paid" : { "type" : "boolean" }, @@ -33204,12 +33210,12 @@ "complete" : { "type" : "boolean" }, - "potentialMatch" : { - "type" : "boolean" - }, "notes" : { "type" : "string" }, + "potentialMatch" : { + "type" : "boolean" + }, "timestampEditable" : { "type" : "boolean" }, @@ -33295,65 +33301,72 @@ "currencyCode" : { "type" : "string" }, - "status" : { - "type" : "string", - "enum" : [ "FREE", "PENDING", "TO_BE_PAID", "ACQUIRED", "CANCELLED", "CHECKED_IN", "EXPIRED", "INVALIDATED", "RELEASED", "PRE_RESERVED" ] - }, "formattedNetPrice" : { "type" : "string" }, "extReference" : { "type" : "string" }, + "status" : { + "type" : "string", + "enum" : [ "FREE", "PENDING", "TO_BE_PAID", "ACQUIRED", "CANCELLED", "CHECKED_IN", "EXPIRED", "INVALIDATED", "RELEASED", "PRE_RESERVED" ] + }, "tags" : { "type" : "array", "items" : { "type" : "string" } }, - "assigned" : { - "type" : "boolean" - }, "eventId" : { "type" : "integer", "format" : "int32" }, - "firstName" : { + "vatCts" : { + "type" : "integer", + "format" : "int32" + }, + "email" : { "type" : "string" }, - "lastName" : { + "uuid" : { "type" : "string" }, + "assigned" : { + "type" : "boolean" + }, + "additionalFields" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, "userLanguage" : { "type" : "string" }, - "categoryId" : { - "type" : "integer", - "format" : "int32" + "checkedIn" : { + "type" : "boolean" + }, + "firstName" : { + "type" : "string" + }, + "lastName" : { + "type" : "string" }, "subscriptionId" : { "type" : "string", "format" : "uuid" }, - "srcPriceCts" : { - "type" : "integer", - "format" : "int32" - }, - "finalPriceCts" : { - "type" : "integer", - "format" : "int32" + "lockedAssignment" : { + "type" : "boolean" }, - "vatCts" : { + "srcPriceCts" : { "type" : "integer", "format" : "int32" }, - "discountCts" : { + "categoryId" : { "type" : "integer", "format" : "int32" }, - "email" : { - "type" : "string" - }, "ticketsReservationId" : { "type" : "string" }, @@ -33361,20 +33374,13 @@ "type" : "string", "enum" : [ "NONE", "INCLUDED", "NOT_INCLUDED", "INCLUDED_EXEMPT", "NOT_INCLUDED_EXEMPT", "CUSTOM_INCLUDED_EXEMPT", "CUSTOM_NOT_INCLUDED_EXEMPT", "INCLUDED_NOT_CHARGED", "NOT_INCLUDED_NOT_CHARGED" ] }, - "additionalFields" : { - "type" : "object", - "additionalProperties" : { - "type" : "string" - } - }, - "uuid" : { - "type" : "string" - }, - "lockedAssignment" : { - "type" : "boolean" + "finalPriceCts" : { + "type" : "integer", + "format" : "int32" }, - "checkedIn" : { - "type" : "boolean" + "discountCts" : { + "type" : "integer", + "format" : "int32" }, "creation" : { "type" : "string", @@ -33514,6 +33520,9 @@ } } }, + "overlap" : { + "type" : "boolean" + }, "duration" : { "type" : "object", "properties" : { @@ -33561,9 +33570,6 @@ "type" : "string", "format" : "date-time" }, - "overlap" : { - "type" : "boolean" - }, "instant" : { "type" : "string", "format" : "date-time" @@ -33653,35 +33659,39 @@ "type" : "integer", "format" : "int32" }, + "subscriptionDescriptorId" : { + "type" : "string", + "format" : "uuid" + }, "status" : { "type" : "string", "enum" : [ "WAITING", "RETRY", "IN_PROCESS", "SENT", "ERROR" ] }, - "organizationId" : { - "type" : "integer", - "format" : "int32" - }, "eventId" : { "type" : "integer", "format" : "int32" }, - "checksum" : { + "cc" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "subject" : { "type" : "string" }, + "organizationId" : { + "type" : "integer", + "format" : "int32" + }, "attempts" : { "type" : "integer", "format" : "int32" }, - "recipient" : { + "checksum" : { "type" : "string" }, - "cc" : { - "type" : "array", - "items" : { - "type" : "string" - } - }, - "subject" : { + "recipient" : { "type" : "string" }, "htmlMessage" : { @@ -33694,10 +33704,6 @@ "type" : "string", "enum" : [ "subscription", "event" ] }, - "subscriptionDescriptorId" : { - "type" : "string", - "format" : "uuid" - }, "requestTimestamp" : { "type" : "string", "format" : "date-time" @@ -33756,6 +33762,10 @@ "type" : "integer", "format" : "int32" }, + "subscriptionDescriptorId" : { + "type" : "string", + "format" : "uuid" + }, "inputField" : { "type" : "boolean" }, @@ -33768,9 +33778,6 @@ "checkboxField" : { "type" : "boolean" }, - "euVat" : { - "type" : "boolean" - }, "maxLengthDefined" : { "type" : "boolean" }, @@ -33784,6 +33791,13 @@ "type" : "integer", "format" : "int32" }, + "required" : { + "type" : "boolean" + }, + "eventId" : { + "type" : "integer", + "format" : "int32" + }, "minLength" : { "type" : "integer", "format" : "int32" @@ -33792,15 +33806,17 @@ "type" : "integer", "format" : "int32" }, - "required" : { - "type" : "boolean" - }, "inputType" : { "type" : "string" }, - "eventId" : { - "type" : "integer", - "format" : "int32" + "countryField" : { + "type" : "boolean" + }, + "restrictedValues" : { + "type" : "array", + "items" : { + "type" : "string" + } }, "additionalServiceId" : { "type" : "integer", @@ -33816,14 +33832,7 @@ "dateOfBirth" : { "type" : "boolean" }, - "subscriptionDescriptorId" : { - "type" : "string", - "format" : "uuid" - }, - "countryField" : { - "type" : "boolean" - }, - "restrictedValues" : { + "disabledValues" : { "type" : "array", "items" : { "type" : "string" @@ -33832,11 +33841,8 @@ "editable" : { "type" : "boolean" }, - "disabledValues" : { - "type" : "array", - "items" : { - "type" : "string" - } + "euVat" : { + "type" : "boolean" } } }, @@ -33925,9 +33931,6 @@ "checkboxField" : { "type" : "boolean" }, - "euVat" : { - "type" : "boolean" - }, "maxLengthDefined" : { "type" : "boolean" }, @@ -33940,10 +33943,13 @@ "inputType" : { "type" : "string" }, + "countryField" : { + "type" : "boolean" + }, "dateOfBirth" : { "type" : "boolean" }, - "countryField" : { + "euVat" : { "type" : "boolean" } } @@ -34051,14 +34057,14 @@ "$ref" : "#/components/schemas/PollOptionStatistics" } }, - "participationPercentage" : { - "type" : "string" - }, "optionStatistics" : { "type" : "array", "items" : { "$ref" : "#/components/schemas/StatisticDetail" } + }, + "participationPercentage" : { + "type" : "string" } } }, @@ -34702,6 +34708,9 @@ "PromoCodeDiscountWithFormattedTimeAndAmount" : { "type" : "object", "properties" : { + "dynamic" : { + "type" : "boolean" + }, "id" : { "type" : "integer", "format" : "int32" @@ -34709,12 +34718,6 @@ "currencyCode" : { "type" : "string" }, - "dynamic" : { - "type" : "boolean" - }, - "description" : { - "type" : "string" - }, "formattedStart" : { "type" : "string" }, @@ -34724,11 +34727,30 @@ "formattedDiscountAmount" : { "type" : "string" }, + "eventId" : { + "type" : "integer", + "format" : "int32" + }, + "utcEnd" : { + "type" : "string", + "format" : "date-time" + }, + "description" : { + "type" : "string" + }, + "categories" : { + "uniqueItems" : true, + "type" : "array", + "items" : { + "type" : "integer", + "format" : "int32" + } + }, "organizationId" : { "type" : "integer", "format" : "int32" }, - "eventId" : { + "maxUsage" : { "type" : "integer", "format" : "int32" }, @@ -34739,10 +34761,6 @@ "type" : "string", "format" : "date-time" }, - "utcEnd" : { - "type" : "string", - "format" : "date-time" - }, "discountAmount" : { "type" : "integer", "format" : "int32" @@ -34759,17 +34777,8 @@ "type" : "integer", "format" : "int32" }, - "maxUsage" : { - "type" : "integer", - "format" : "int32" - }, - "categories" : { - "uniqueItems" : true, - "type" : "array", - "items" : { - "type" : "integer", - "format" : "int32" - } + "fixedAmount" : { + "type" : "boolean" }, "currentlyValid" : { "type" : "boolean" @@ -34779,9 +34788,6 @@ }, "expired" : { "type" : "boolean" - }, - "fixedAmount" : { - "type" : "boolean" } } }, @@ -35084,6 +35090,9 @@ "type" : "integer", "format" : "int32" }, + "formattedEnd" : { + "type" : "string" + }, "status" : { "type" : "string", "enum" : [ "DRAFT", "PUBLIC", "DISABLED" ] @@ -35091,9 +35100,13 @@ "shortName" : { "type" : "string" }, - "formattedEnd" : { + "fileBlobId" : { "type" : "string" }, + "notAllocatedTickets" : { + "type" : "integer", + "format" : "int32" + }, "organizationId" : { "type" : "integer", "format" : "int32" @@ -35105,12 +35118,17 @@ "enum" : [ "STRIPE", "ON_SITE", "OFFLINE", "NONE", "ADMIN", "PAYPAL", "MOLLIE", "SAFERPAY" ] } }, - "fileBlobId" : { + "visibleForCurrentUser" : { + "type" : "boolean" + }, + "displayStatistics" : { + "type" : "boolean" + }, + "formattedBegin" : { "type" : "string" }, - "notAllocatedTickets" : { - "type" : "integer", - "format" : "int32" + "warningNeeded" : { + "type" : "boolean" }, "availableSeats" : { "type" : "integer", @@ -35122,18 +35140,6 @@ }, "expired" : { "type" : "boolean" - }, - "warningNeeded" : { - "type" : "boolean" - }, - "formattedBegin" : { - "type" : "string" - }, - "visibleForCurrentUser" : { - "type" : "boolean" - }, - "displayStatistics" : { - "type" : "boolean" } } }, @@ -35155,24 +35161,17 @@ "url" : { "type" : "string" }, - "begin" : { - "type" : "string" - }, "end" : { "type" : "string" }, - "latitude" : { - "type" : "string" - }, - "longitude" : { + "begin" : { "type" : "string" }, "imageUrl" : { "type" : "string" }, - "apiVersion" : { - "type" : "integer", - "format" : "int32" + "latitude" : { + "type" : "string" }, "descriptions" : { "type" : "array", @@ -35180,6 +35179,13 @@ "$ref" : "#/components/schemas/PublicEventDescription" } }, + "longitude" : { + "type" : "string" + }, + "apiVersion" : { + "type" : "integer", + "format" : "int32" + }, "oneDay" : { "type" : "boolean" }