* Fertigstellung Buchungsformular (#1321) (SternTours-CRM-API-Anbindung, Mailversand, Validierung, Dynamische Preisberechnung, Persistierung von Buchungsinformationen)

* Fehler bei der Preisberechnung behoben
* Farbschema geändert (Kevin Adametz)

git-svn-id: http://78.47.251.156/svn/dev/sterntours-3@3289 f459cee4-fb09-11de-96c3-f9c5f16c3c76
This commit is contained in:
uli 2017-02-14 11:26:49 +00:00
parent dde3b91724
commit 3a28866cd2
36 changed files with 2200 additions and 268 deletions

View file

@ -21,7 +21,6 @@
{% stylesheets
'bundles/app/css/bootstrap-3.3.7.css'
'bundles/app/css/custom.css'
'bundles/app/css/booking.css'
filter='cssrewrite'
%}
<link rel="stylesheet" href="{{ asset_url }}"/>

View file

@ -0,0 +1,20 @@
<table class="st-booking-table">
<tbody>
{% for summary_entry in summary %}
<tr>
<td class="st-position-price-col">
{{ summary_entry.value|number_format(2) }}
</td>
<td class="st-position-name-col">
{{ summary_entry.label|raw }}
</td>
</tr>
{% endfor %}
<tr class="st-total-tr">
<td class="st-position-price-col">
<span class="st-total-price">= {{ total_price|number_format(2) }} €</span>
</td>
<td class="st-position-name-col">Gesamtpreis der Reise</td>
</tr>
</tbody>
</table>

View file

@ -0,0 +1,11 @@
{# @var booking_request \AppBundle\Entity\BookingRequest #}
Sehr geehrte{{ booking_request.salutation == 1 ? 'r Herr' : ' Frau' }} {{ booking_request.lastName }},
vielen Dank für Ihren Buchungsauftrag. Dieser wird schnellstmöglich bearbeitet und stellt noch keine{#
#} Buchungsbestätigung dar. Bitte prüfen Sie noch einmal Ihre Angaben und kontaktieren Sie uns bitte, wenn ein Fehler{#
#} enthalten ist.
{% include 'default/email/components/bookingSummary.txt.twig' %}
{% include 'default/email/components/signature.txt.twig' %}

View file

@ -0,0 +1,7 @@
FOLGENDE REISE WURDE GEBUCHT:
URL: {{ travel_program_url }}
CRM: {{ crm_url }}
{% include 'default/email/components/bookingSummary.txt.twig' %}

View file

@ -0,0 +1,71 @@
{# @var booking_request \AppBundle\Entity\BookingRequest #}
=====================================================================================
Reisedaten:
=====================================================================================
Reiseprogramm: {{ travel_date.travelProgram.title }} ({{ travel_date.name }})
Kategorie: Standard
Reisezeitraum: {{ travel_date.start|date }} - {{ travel_date.end|date }}
Abfahrts-/Abflugort: {{ booking_request.departure.name }} {{ booking_request.departure.extraCharge|number_format(2) }} € p.P.
{% for room in booking_price_info['rooms'] %}
1x {{ room['name'] }} [Personen: {{ room.adults }} x {{ room['price']|number_format(2) }} €]
{% endfor %}
{{ booking_request.departure.extraCharge < 0 ? 'Aufschlag' : 'Abzug' }} Abfahrts-/Abflugort {{ booking_request.departure.name }}{#
#} {{ booking_request.travelerCount }} x {{ booking_request.departure.extraCharge|number_format(2) }} €: {{
(booking_request.travelerCount * booking_request.departure.extraCharge)|number_format(2) }} €
{% for insuranceInfo in booking_price_info['insurances'] %}
{{ insuranceInfo['count'] }}x RV {{ insuranceInfo['insurance'].name }} ({{ insuranceInfo['insurancePrice'].code -}}
) {{ insuranceInfo['insurancePriceValue']|number_format(2) }} €: {{ (insuranceInfo['count'] *
insuranceInfo['insurancePriceValue'])|number_format(2) }} €
{% endfor %}
{% for option in booking_request.travelOptions %}
{{ booking_request.travelerCount }}x zugebuchte Leistung (Erwachsener): {{ option.name }} {{ option.price|number_format(2) -}}
€: {{ (booking_request.travelerCount * option.price|number_format(2)) }}
{% endfor %}
{% for classOption in booking_price_info['classOptions'] %}
{{ classOption['count'] }}x {{ classOption['name'] }} {{ classOption['price']|number_format(2) }} €: {{
(classOption['count'] * classOption['price'])|number_format(2) }} €
{% endfor %}
Gesamtpreis: {{ booking_price_info['total']|number_format(2) }}
=====================================================================================
Reiseleistungen:
=====================================================================================
Eingeschlossene Leistungen:
{% for travel_program_service in travel_date.travelProgram.included|split('\n') %}
[x] {{ travel_program_service|raw }}
{% endfor %}
Nicht eingeschlossene, zubuchbare Leistungen:
{% for travel_program_service in travel_date.travelProgram.excluded|split('\n') %}
[o] {{ travel_program_service|raw }}
{% endfor %}
=====================================================================================
Reiseanmelder{% if booking_request.salutation == 2 %}in{% endif %}
=====================================================================================
Vorname: {{ booking_request.firstName }}
Nachname: {{ booking_request.lastName }}
Adresse: {{ booking_request.streetAddress }}
PLZ: {{ booking_request.zipCode }}
Ort: {{ booking_request.city }}
Telefonnummer: {{ booking_request.phone }}
Fax: {{ booking_request.fax ?? 'keine Angabe' }}
=====================================================================================
Reiseteilnehmer:
#) Geschlecht, Vorname, Nachname, Geburtsdatum
=====================================================================================
{% for traveler in booking_request.travelers|slice(0, booking_request.travelerCount) %}
{{ loop.index }}) {{ traveler.sex == 1 ? 'männlich' : 'weiblich' }}, {{ traveler.firstName }}, {{ traveler.lastName -}}
, {{ traveler.birthDate|date }}
{% endfor %}
=====================================================================================
Mitteilungen / Sonstiges:
=====================================================================================
{{ booking_request.notes ?? '-' }}

View file

@ -0,0 +1,19 @@
Mit freundlichen Grüßen
Ihr Team von STERN TOURS
--
STERN TOURS Travelservice GmbH
Uhlandstr. 137
10717 Berlin
Geschäftsführer: Thomas Stern
E-Mail: stern@stern-tours.de
Tel.: 030 / 700 94 100
Fax: 030 / 700 94 1044
Registergericht: Amtsgericht Charlottenburg
Registernummer: HRB 67111
Steuernummer: 27/016/10728
UST-Ident.-Nr.: DE192609253
Finanzamt: Wilmersdorf

View file

@ -0,0 +1,19 @@
{%- block form_field -%}
{{- form_label(form, label, opt ?? {}) -}}
{{- form_widget(form, opt ?? {}) -}}
{{- form_errors(form) -}}
{%- endblock form_field -%}
{%- block form_field_pho -%}
{%- set opt = opt|merge({
label_attr: (opt.label_attr ?? {})|merge({class: (opt.label_attr.class|default('') ~ ' sr-only')|trim}),
attr: (opt.attr ?? {})|merge({placeholder: opt.attr.placeholder|default(
(form.vars.translation_domain is same as(false) ? label : label|trans({}, form.vars.translation_domain)) ~
((opt.required ?? form.vars.required) ? ' *' : '')
)})
}) -%}
{#{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' sr-only')|trim}) -%}#}
{{- form_label(form, label, opt) -}}
{{- form_widget(form, opt) -}}
{{- form_errors(form, opt) -}}
{%- endblock form_field_pho -%}

View file

@ -0,0 +1,32 @@
{% use 'form_div_layout.html.twig' with
choice_widget_collapsed as base_choice_widget_collapsed,
checkbox_widget as base_checkbox_widget,
radio_widget as base_radio_widget
%}
{% use 'bootstrap_3_layout.html.twig' %}
{% block choice_widget_collapsed -%}
{% set attr = attr|merge({
class: (attr.class|default('') ~ ' selectpicker')|trim,
'data-style': attr['data-style']|default('btn-white'),
'data-dropout': attr['data-dropout']|default('false')
}) %}
<div class="dropdown">
{{- block('base_choice_widget_collapsed') -}}
</div>
{%- endblock %}
{% block checkbox_widget -%}
{{- block('base_checkbox_widget') -}}
{%- endblock checkbox_widget %}
{% block radio_widget -%}
{{- block('base_radio_widget') -}}
{%- endblock radio_widget %}
{% block form_label -%}
{%- if required -%}
{%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' st-required')|trim}) -%}
{%- endif -%}
{{- parent() -}}
{%- endblock form_label %}

View file

@ -1,4 +1,19 @@
{% extends 'base.html.twig' %}
{% form_theme form 'default/form/theme.html.twig' %}
{% block stylesheets %}
{{ parent() }}
{% stylesheets 'bundles/app/css/booking.css' filter='cssrewrite' %}
<link rel="stylesheet" href="{{ asset_url }}"/>
{% endstylesheets %}
{% endblock %}
{% block javascripts %}
{{ parent() }}
{% javascripts '@AppBundle/Resources/public/js/booking.js' %}
<script src="{{ asset_url }}"></script>
{% endjavascripts %}
{% endblock %}
{% block breadcrumb %}
{{ include('default/components/breadcrumb.html.twig') }}
@ -21,7 +36,10 @@
<div id="booking_form" class="booking_form">
<form id="contactform" class="" action="#" name="contactform" method="post">
<form class="st-booking-form" method="post">
{{ form_errors(form) }}
<div id="message"></div>
<div class="form-box">
@ -46,30 +64,27 @@
<tr>
<td>{{ form_label(form.departure, 'Abflugort') }}</td>
<td>
<div class="dropdown">
{{ form_widget(form.departure, {'attr': {
'class': 'selectpicker',
'data-style': 'btn-white'
}}) }}
</div>
{{ form_widget(form.departure) }}
{{ form_errors(form.departure) }}
</td>
</tr>
<tr>
<td>{{ form_label(form.travelerCount, 'Reiseteilnehmer') }}</td>
<td><div class="dropdown">
Erwachsene<br>
{{ form_widget(form.travelerCount) }}
</div>
<td>
Erwachsene<br>
{{ form_widget(form.travelerCount) }}
{{ form_errors(form.travelerCount) }}
</td>
</tr>
<tr>
<td>Reiseversicherung</td>
<td>
<div class="radio">
<input id="radio1" type="radio" name="radio">
<label for="radio1">
keine Reiseversicherung
</label>
<input id="st-no-insurance-opt" type="radio" value=""
name="{{ form.insurance.vars.full_name }}"
{% if form.insurance.vars.value == '' %}checked{% endif %}
>
<label for="st-no-insurance-opt">keine Reiseversicherung</label>
</div>
{% for insuranceForm in form.insurance %}
@ -78,6 +93,8 @@
'insurance': form.insurance.vars.choices[insuranceForm.vars.value].data
} %}
{% endfor %}
{{ form_errors(form.insurance) }}
</td>
</tr>
<tr>
@ -100,7 +117,7 @@
{% for price in travel_date.prices %}
{# @var price \AppBundle\Entity\TravelPeriodPrice #}
<li>
p.P. {{ price.priceComfort|number_format(2) }}
p.P. {{ price.effectiveComfortPrice|number_format(2) }}
{{ price_type_by_id[price.priceType.id].name }}
</li>
{% endfor %}
@ -128,23 +145,12 @@
<div class="panel">
<div class="panel-body">
<h3>Ihr gewähltes Angebot</h3>
<table class="st-booking-table">
<tbody>
<tr>
<td class="st-position-price-col">-700,00 €</td>
<td class="st-position-name-col">
Abzug für Abfahrts-/Abflugort "Eigenanreise" (2 x -350,00 €):
<strong>-700,00 €</strong>
</td>
</tr>
<tr class="st-total-tr">
<td class="st-position-price-col">
<span class="st-total-price">= 3.921,68 €</span>
</td>
<td class="st-position-name-col">Gesamtpreis der Reise</td>
</tr>
</tbody>
</table>
<div class="st-booking-summary">
{% include 'default/components/booking/summary.html.twig' with {
'summary': summary,
'total_price': total_price
} %}
</div>
</div>
</div>
</div>
@ -197,59 +203,44 @@
</div>
<div class="form-group col-md-12 col-sm-12 col-xs-12">
<div class="dropdown">
<select name="salutation" id="salutation" class="selectpicker" data-style="btn-white" data-dropup-auto="false">
<option value="" selected="selected">Anrede (Bitte wählen)</option>
<option value="1">Herr</option>
<option value="2">Frau</option>
</select>
</div>
{{ form_field_pho(form.salutation, 'Anrede', {'label_attr': {class: 'sr-only'}}) }}
</div>
<div class="col-md-6 col-sm-6 col-xs-12">
<input type="text" name="firstname" id="firstname" class="form-control" placeholder="Vorname *">
{#<input type="text" name="firstname" id="firstname" class="form-control" placeholder="Vorname *">#}
{{ form_field_pho(form.firstName, 'Vorname') }}
</div>
<div class="col-md-6 col-sm-6 col-xs-12">
<input type="text" name="lastname" id="lastname" class="form-control" placeholder="Nachname *">
{{ form_field_pho(form.lastName, 'Nachname') }}
</div>
<div class="col-md-12 col-sm-12 col-xs-12">
<input type="text" name="street" id="street" class="form-control" placeholder="Straße, Hausnummer">
</div>
<div class="col-md-6 col-sm-6 col-xs-12">
<input type="text" name="plz" id="plz" class="form-control" placeholder="PLZ">
{{ form_field_pho(form.streetAddress, 'Straße, Hausnummer') }}
</div>
<div class="col-md-6 col-sm-6 col-xs-12">
<input type="text" name="ort" id="ort" class="form-control" placeholder="Ort">
{{ form_field_pho(form.zipCode, 'PLZ') }}
</div>
<div class="col-md-6 col-sm-6 col-xs-12">
{{ form_field_pho(form.city, 'Ort') }}
</div>
<div class="form-group col-md-12 col-sm-12 col-xs-12">
<div class="dropdown">
<select name="country" class="selectpicker" data-style="btn-white" data-dropup-auto="false">
<option value="" selected="selected">Land (Bitte wählen)</option>
<option value="27">Deutschland</option>
<option value="34">Österreich</option>
<option value="181">Schweiz</option>
<option value="196">Niederlande</option>
<option value="197">Sonstiges</option>
</select>
</div>
{{ form_field_pho(form.nation, 'Land') }}
</div>
<div class="col-md-6 col-sm-6 col-xs-12">
<input type="text" name="firstname" id="firstname" class="form-control" placeholder="Telefon tagsüber *">
{{ form_field_pho(form.phone, 'Telefon tagsüber') }}
</div>
<div class="col-md-6 col-sm-6 col-xs-12">
<input type="text" name="lastname" id="lastname" class="form-control" placeholder="Fax (optional)">
{{ form_field_pho(form.fax, 'Fax (optional)') }}
</div>
<div class="col-md-12 col-sm-12 col-xs-12">
<input type="text" name="email" id="email" class="form-control" placeholder="E-Mail-Adresse *">
{{ form_field_pho(form.email, 'E-Mail-Adresse') }}
</div>
</div>
@ -274,53 +265,36 @@
<th>Geburtsdatum (TT.MM.JJJJ)</th>
</tr>
</thead>
<tbody>
<tr>
<td data-title="Nr.">
<button class="btn btn-primary btn-sm border-radius">1</button>
</td>
<td data-title="Geschlecht">
<div class="dropdown">
<select name="salutation" id="salutation1" class="selectpicker" data-style="btn-white" data-dropup-auto="false">
<option value="" selected="selected">Anrede (Bitte wählen)</option>
<option value="1">Herr</option>
<option value="2">Frau</option>
</select>
</div>
</td>
<td data-title="Vorname">
<input type="text" name="firstname" id="firstname1" class="form-control" placeholder="Vorname">
</td>
<td data-title="Nachname">
<input type="text" name="firstname" id="firstname1" class="form-control" placeholder="Nachname">
</td>
<td data-title="Geburtsdatum">
<input type="text" name="firstname" id="firstname1" class="form-control" placeholder="Geburtsdatum">
</td>
</tr>
<tr>
<td data-title="Nr.">
<button class="btn btn-primary btn-sm border-radius">2</button>
</td>
<td data-title="Geschlecht">
<div class="dropdown">
<select name="salutation" id="salutation2" class="selectpicker" data-style="btn-white" data-dropup-auto="false">
<option value="" selected="selected">Anrede (Bitte wählen)</option>
<option value="2">Herr</option>
<option value="2">Frau</option>
</select>
</div>
</td>
<td data-title="Vorname">
<input type="text" name="firstname" id="firstname2" class="form-control" placeholder="Vorname">
</td>
<td data-title="Nachname">
<input type="text" name="firstname" id="firstname2" class="form-control" placeholder="Nachname">
</td>
<td data-title="Geburtsdatum">
<input type="text" name="firstname" id="firstname2" class="form-control" placeholder="Geburtsdatum">
</td>
</tr>
<tbody class="st-travelers">
{% for traveler_form in form.travelers %}
<tr class="st-traveler st-traveler-{{ loop.index }}"
data-st-traveler-index="{{ loop.index }}"
style="display: none;"
>
<td>
<button class="btn btn-primary btn-sm border-radius st-traveller-index"
type="button"
>
{{ loop.index ?? '' }}
</button>
</td>
<td>
{{ form_field_pho(traveler_form.sex, 'Geschlecht', {
required: false
}) }}
</td>
<td>
{{ form_field_pho(traveler_form.firstName, 'Vorname') }}
</td>
<td>
{{ form_field_pho(traveler_form.lastName, 'Nachname') }}
</td>
<td>
{{ form_field_pho(traveler_form.birthDate, 'Geburtsdatum') }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
@ -335,7 +309,10 @@
<h5>Mitteilungen / Sonstiges (optional)</h5>
</div>
<div class="col-md-12 col-sm-12 col-xs-12">
<textarea class="form-control" name="comments" id="comments" rows="6" placeholder=""></textarea>
{{ form_field(form.notes, 'Mitteilungen / Sonstiges (optional)', {
'label_attr': {'class': 'sr-only'},
'attr': {'rows': '6'}
}) }}
</div>
</div>
</div><!-- end form-box -->
@ -345,7 +322,9 @@
<div class="col-md-12 col-sm-12 col-xs-12">
<h5>Zahlung</h5>
<p>Die gewünschte Zahlungsart (Rechnung, Überweisung, Sofortüberweisung, Kreditkarten, Barzahlung) stimmen wir mit Ihnen im Anschluss an Ihre Buchung ab.</p>
<p>Die gewünschte Zahlungsart (Rechnung, Überweisung, Sofortüberweisung, Kreditkarten,
Barzahlung) stimmen wir mit Ihnen im Anschluss an Ihre Buchung ab.
</p>
</div>
@ -357,18 +336,28 @@
<div class="col-md-12 col-sm-12 col-xs-12">
<h5>Allgemeine Geschäftsbedingungen</h5>
<div class="checkbox">
<input id="checkbox4" type="checkbox">
<label for="checkbox4">
Ich habe alle Daten und Angaben auf Richtigkeit überprüft. Ich habe die <a href="#">Allgemeinen Geschäftsbedingungen des Reiseveranstalters</a> SKR sowie die <a href="#">Allgemeinen Geschäftsbedingungen des Reisevermittlers</a> gelesen und akzeptiert. Zugleich erkenne ich diese für alle Reiseteilnehmer an.
{{ form_widget(form.acceptTerms) }}
<label for="{{ form.acceptTerms.vars.id }}">
Ich habe alle Daten und Angaben auf Richtigkeit überprüft. Ich habe die
<a href="#">Allgemeinen Geschäftsbedingungen des Reiseveranstalters</a> SKR
sowie die <a href="#">Allgemeinen Geschäftsbedingungen des Reisevermittlers</a>
gelesen und akzeptiert. Zugleich erkenne ich diese für alle Reiseteilnehmer an.
</label>
{{ form_errors(form.acceptTerms) }}
</div>
</div>
<div class="col-md-12 col-sm-12 col-xs-12">
<button type="submit" value="SEND" id="submit" class="aligncenter btn btn-primary btn-lg border-radius">kostenpflichtig<br class="visible-xs"> buchen</button>
<button type="submit" value="SEND" id="submit"
class="aligncenter btn btn-primary btn-lg border-radius"
>
kostenpflichtig<br class="visible-xs"> buchen
</button>
</div>
</div>
</div><!-- end form-box -->
{{ form_rest(form) }}
</form>
</div><!-- end contact-form -->

View file

@ -0,0 +1,5 @@
{% extends 'base.html.twig' %}
{% block body %}
<h1>Vielen Dank für Ihren Buchungsauftrag!</h1>
{% endblock %}

View file

@ -5,12 +5,12 @@ imports:
# Put parameters here that don't need to change on each machine where the app is deployed
# http://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
locale: en
#parameters:
# locale: en
framework:
#esi: ~
#translator: { fallbacks: ["%locale%"] }
translator: { fallbacks: ["%locale%", "en"] }
secret: "%secret%"
router:
resource: "%kernel.root_dir%/config/routing.yml"
@ -31,8 +31,8 @@ framework:
fragments: ~
http_method_override: true
assets: ~
profiler:
collect: false
#profiler:
# collect: false
# Twig Configuration
twig:
@ -88,7 +88,7 @@ assetic:
cssrewrite: ~
stof_doctrine_extensions:
default_locale: en_US
default_locale: de_DE
orm:
default:
tree: true

View file

@ -21,6 +21,9 @@ monolog:
console:
type: console
channels: [!event, !doctrine]
browser_console:
type: browser_console
level: debug
# uncomment to get logging in your browser
# you may have to allow bigger header sizes in your Web server configuration
#firephp:

View file

@ -19,3 +19,4 @@ parameters:
secret: ThisTokenIsNotSoSecretChangeIt
st_cache_driver: array
locale: de_DE

View file

@ -14,4 +14,22 @@ services:
- "@doctrine.orm.entity_manager"
- "@controller_resolver"
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
app.twig_extension:
class: AppBundle\Twig\AppExtension
#public: false
arguments:
- '@twig'
tags:
- { name: twig.extension }
app.booking_exporter:
class: AppBundle\Export\SternToursCrmBookingExporter
arguments:
- '@monolog.logger'
app.booking_request_validator:
class: AppBundle\Validator\BookingRequestValidator
tags:
- {name: validator.constraint_validator }

View file

@ -10,14 +10,32 @@ namespace AppBundle\Controller;
use AppBundle\Entity\BookingRequest;
use AppBundle\Entity\BreadcrumbEntry;
use AppBundle\Entity\Page;
use AppBundle\Entity\TravelDate;
use AppBundle\Entity\Traveler;
use AppBundle\Entity\TravelPeriodPrice;
use AppBundle\Entity\TravelPeriodPriceType;
use AppBundle\Form\BookingRequestType;
use AppBundle\Util;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class BookingController extends Controller
{
public function indexAction(Page $travelProgramPage, Request $request)
/** @var TravelPeriodPriceType[] $priceTypeById */
private $priceTypeById;
/**
* The routing for this action is entirely controlled by KernelControllerListener!
*
* @param Page $travelProgramPage
* @param Request $request
* @param $action
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
* @throws \Exception
*/
public function indexAction(Page $travelProgramPage, $action, Request $request)
{
$travelProgram = $travelProgramPage->getTravelProgram();
if (!$request->query->has('nr'))
@ -26,6 +44,8 @@ class BookingController extends Controller
}
$this->getDoctrine()->getRepository('AppBundle:TravelPeriod')->getTrueTravelPeriods($travelProgram);
$this->priceTypeById = $this->getDoctrine()->getRepository('AppBundle:TravelPeriodPriceType')->findAllIndexedById();
// #TODO Consider changing key of travel dates
foreach ($travelProgram->getTravelDates() as $curTravelDate)
{
@ -40,31 +60,355 @@ class BookingController extends Controller
throw $this->createNotFoundException();
}
$form = $this->createForm(BookingRequestType::class, null, [
/** @var BookingRequest $bookingRequest */
$bookingRequest = new BookingRequest();
if ($request->getMethod() != 'POST')
{
$bookingRequest->setTravelerCount(2);
$bookingRequest->setDeparture($travelDate->getDepartures()[0]);
}
$form = $this->createForm(BookingRequestType::class, $bookingRequest, [
'travel_date' => $travelDate,
'travel_program' => $travelProgram
]);
//$form->submit([]);
if ($request->getMethod() == 'POST')
{
$form->handleRequest($request);
$bookingRequest = $form->getData();
}
$htmlSummary = [];
$bookingPriceInfo = [];
$totalPrice = $this->calculatePrice($travelDate, $bookingRequest, $htmlSummary, $bookingPriceInfo);
$priceTypeById = $this->getDoctrine()->getRepository('AppBundle:TravelPeriodPriceType')->findAllIndexedById();
if ($action == 'buchen')
{
$breadcrumbEntries = Util::createBreadcrumb($travelProgramPage);
$breadcrumbEntries[] = new BreadcrumbEntry('Buchen', $travelProgramPage->getUrlPath() .'/buchen');
$breadcrumbEntries = Util::createBreadcrumb($travelProgramPage);
$breadcrumbEntries[] = new BreadcrumbEntry('Buchen', $travelProgramPage->getUrlPath() .'/buchen');
if ($request->getMethod() == 'POST' && $form->isValid())
{
$booking = $this->getDoctrine()->getRepository('AppBundle:TravelBooking')->createFromBookingRequest(
$bookingRequest, $travelDate, $bookingPriceInfo);
$em = $this->getDoctrine()->getManager();
$em->persist($booking);
$em->flush();
return $this->render('default/pages/booking.html.twig', [
'base_dir' => realpath($this->getParameter('kernel.root_dir').'/..').DIRECTORY_SEPARATOR,
'page' => $travelProgramPage,
'travel_program' => $travelProgram,
'travel_date' => $travelDate,
'form' => $form->createView(),
'price_type_by_id' => $priceTypeById,
'breadcrumb_entries' => $breadcrumbEntries,
]);
$crmBookingUrl = $this->get('app.booking_exporter')->process($bookingRequest, $travelDate, $bookingPriceInfo);
$this->get('mailer')->send(\Swift_Message::newInstance()
->setSubject('Ihr Buchungsauftrag bei STERN TOURS')
->setFrom('stern@stern-tours.de', 'STERN TOURS')
->setTo($bookingRequest->getEmail())
->setBody(
$this->renderView('default/email/bookingConfirmationEmail.txt.twig', [
'base_dir' => realpath($this->getParameter('kernel.root_dir').'/..').DIRECTORY_SEPARATOR,
'booking_request' => $bookingRequest,
'booking_price_info' => $bookingPriceInfo,
'travel_date' => $travelDate,
'breadcrumb_entries' => $breadcrumbEntries,
]),
'text/plain', 'utf-8'
)
);
$this->get('mailer')->send(\Swift_Message::newInstance()
->setSubject('BUCHUNG: '. $travelProgram->getTitle() .'('. $travelDate->getName() .')')
->setFrom('stern@stern-tours.de', 'STERN TOURS')
->setTo('sternt@stern-tours.de')
->setBody(
$this->renderView('default/email/bookingServiceEmail.txt.twig', [
'base_dir' => realpath($this->getParameter('kernel.root_dir').'/..').DIRECTORY_SEPARATOR,
'crm_url' => $crmBookingUrl .'/edit',
'travel_program_url' => 'http' . (($_SERVER['SERVER_PORT'] == 443) ? 's://' : '://') .
$_SERVER['HTTP_HOST'] . $travelProgramPage->getUrlPath(),
'booking_request' => $bookingRequest,
'booking_price_info' => $bookingPriceInfo,
'travel_date' => $travelDate,
'breadcrumb_entries' => $breadcrumbEntries,
]),
'text/plain', 'utf-8'
)
);
// #TODO This will lead to multiple bookings due to multiple form submission. Redirect instead!
return $this->render('default/pages/bookingConfirmation.html.twig', [
'base_dir' => realpath($this->getParameter('kernel.root_dir').'/..').DIRECTORY_SEPARATOR,
'page' => $travelProgramPage,
'breadcrumb_entries' => $breadcrumbEntries,
]);
}
return $this->render('default/pages/booking.html.twig', [
'base_dir' => realpath($this->getParameter('kernel.root_dir').'/..').DIRECTORY_SEPARATOR,
'page' => $travelProgramPage,
'travel_program' => $travelProgram,
'travel_date' => $travelDate,
'form' => $form->createView(),
'price_type_by_id' => $this->priceTypeById,
'breadcrumb_entries' => $breadcrumbEntries,
'summary' => $htmlSummary,
'total_price' => $totalPrice
]);
}
elseif ($action == 'berechne-gesamtpreis')
{
return $this->render('default/components/booking/summary.html.twig', [
'base_dir' => realpath($this->getParameter('kernel.root_dir').'/..').DIRECTORY_SEPARATOR,
'summary' => $htmlSummary,
'total_price' => $totalPrice
]);
}
throw new \Exception('Unknow BookingController action: '. $action);
}
public function calculatePrice()
public function calculatePrice(TravelDate $travelDate, BookingRequest $bookingRequest, &$outHtmlSummary = null,
&$outPriceInfo = null)
{
$ret = 0;
$insuranceAssessmentBasis = 0;
$travelerCount = $bookingRequest->getTravelerCount();
if (isset($outHtmlSummary))
{
$insuranceHtmlSummary = [];
}
if (isset($outPriceInfo))
{
$outPriceInfo['rooms'] = [];
$outPriceInfo['insurances'] = [];
$outPriceInfo['options'] = [];
$outPriceInfo['classOptions'] = [];
}
$departure = Util\DepartureUtil::limitIndividualArrivalPrice($bookingRequest->getDeparture(),
$travelDate->getFlightPrice());
if (isset($outPriceInfo))
{
$outPriceInfo['departure'] = $departure;
}
if ($departure->getExtraCharge() != 0)
{
$insuranceAssessmentBasis += $departure->getExtraCharge();
$a = $travelerCount * $departure->getExtraCharge();
$ret += $a;
if (isset($outHtmlSummary))
{
$outHtmlSummary[] = [
'value' => $a,
'label' => $travelerCount .'x '. ($departure->getExtraCharge() > 0 ? 'Aufschlag' : 'Abzug') .
' für Abfahrts-/Abflugort "'. $departure->getName() .'" <strong>'.
Util::formatPrice($departure->getExtraCharge()) .'</strong>'
];
}
}
foreach ($bookingRequest->getTravelOptions() as $travelOption)
{
$insuranceAssessmentBasis += $travelOption->getPrice();
$a = $travelerCount * $travelOption->getPrice();
$ret += $a;
if (isset($outHtmlSummary))
{
$outHtmlSummary[] = [
'value' => $a,
'label' => $travelerCount .'x zugebuchte Leistung: '. $travelOption->getName() .' <strong>'.
Util::formatPrice($travelOption->getPrice()) .'</strong>'
];
}
if (isset($outPriceInfo))
{
$outPriceInfo['options'][] = $travelOption;
}
}
$persons = [
'total' => $travelerCount,
'adults' => $travelerCount,
'children' => 0
];
$possibleRooms = $this->searchRooms($travelDate->getPrices(), $persons);
if (empty($possibleRooms))
{
if ($travelerCount % 2 == 0)
{
$possibleRooms = $this->splitIntoTwoGroups($travelDate->getPrices(), $persons, 'equal');
}
elseif ($travelerCount >= 3)
{
$possibleRooms = $this->splitIntoTwoGroups($travelDate->getPrices(), $persons, 'move_without_children');
}
}
if ($bookingRequest->getComfort())
{
foreach ($possibleRooms as $room)
{
$insuranceAssessmentBasis += $room['price']->getEffectiveComfortPrice();
$a = $room['persons']['total'] * $room['price']->getEffectiveComfortPrice();
$ret += $a;
if (isset($outHtmlSummary))
{
$outHtmlSummary[] = [
'value' => $a,
'label' => $room['persons']['total'] .'x zugebuchte Leistung: Komfort-Kategorie <strong>'.
Util::formatPrice($room['price']->getEffectiveComfortPrice()) .'</strong>'
];
}
if (isset($outPriceInfo))
{
$outPriceInfo['classOptions'][] = [
'count' => $room['persons']['total'],
'name' => 'zugebuchte Leistung: Komfort (4 Sterne)',
'price' => $room['price']->getEffectiveComfortPrice()
];
}
}
}
$insuranceTotal = 0;
foreach ($possibleRooms as $room)
{
$adultCount = $room['persons']['adults'];
$singleFullPrice = $room['price']->getEffectivePrice();
$roomPrice = $singleFullPrice * $adultCount + $singleFullPrice * $room['persons']['children'];
$singleDiscountPrice = $room['price']->getEffectiveDiscountPrice();
$discount = ($singleDiscountPrice === null) ? 0
: ($adultCount * ($singleDiscountPrice - $singleFullPrice));
$ret += $roomPrice + $discount;
if (isset($outPriceInfo))
{
$outPriceInfo['rooms'][] = [
'name' => $room['priceType']->getName(),
'adults' => $room['persons']['adults'],
'children' => $room['persons']['children'],
'price' => $singleDiscountPrice ?? $singleFullPrice,
'price_children' => $room['price']->getEffectiveChildPrice(),
'price_total' => $roomPrice + $discount,
];
}
if (isset($outHtmlSummary))
{
$label = '1x '. $room['priceType']->getName() .' [Personen: '. $adultCount .' x <strong>'.
Util::formatPrice($singleFullPrice) .'</strong>';
if ($room['persons']['children'] != 0)
{
$label .= ', Kinder: '. $room['persons']['children'] .' x <strong>'.
Util::formatPrice($room['price']->getEffectiveChildPrice()) .'</strong>';
}
$label .= ']';
$outHtmlSummary[] = [
'value' => $roomPrice,
'label' => $label
];
if ($singleDiscountPrice !== null)
{
$outHtmlSummary[] = [
'value' => $discount,
'label' => $adultCount .'x '.
Util::formatPrice($singleFullPrice - $singleDiscountPrice) .' Rabatt'
];
}
if ($bookingRequest->getInsurance() && $adultCount > 0)
{
$curAssessmentBasis = $insuranceAssessmentBasis + ($singleDiscountPrice ?? $singleFullPrice);
$insurancePrice = $this->getDoctrine()->getRepository('AppBundle:TravelInsurancePrice')
->findOneByInsuranceIdAndAssessmentBasis($bookingRequest->getInsurance()->getId(),
$curAssessmentBasis);
$insurancePriceValue = $insurancePrice->getPrice() > 0 ? $insurancePrice->getPrice()
: round($insurancePrice->getPercent() * $curAssessmentBasis / 100, 2);
$a = $adultCount * $insurancePriceValue;
$insuranceTotal += $a;
$ret += $a;
if (isset($insuranceHtmlSummary))
{
$insuranceHtmlSummary[] = [
'value' => $a,
'label' => $adultCount .'x RV '. $bookingRequest->getInsurance()->getName() .' ('.
$insurancePrice->getCode() .') <strong>'. Util::formatPrice($insurancePriceValue) .
'</strong>'
];
}
if (isset($outPriceInfo))
{
$outPriceInfo['insurances'][] = [
'insurance' => $bookingRequest->getInsurance(),
'insurancePriceValue' => $insurancePriceValue,
'insurancePrice' => $insurancePrice,
'count' => $adultCount,
];
}
}
}
}
if (isset($insuranceHtmlSummary))
{
$outHtmlSummary = array_merge($outHtmlSummary, $insuranceHtmlSummary);
}
if (isset($outPriceInfo))
{
$outPriceInfo['total'] = $ret;
$outPriceInfo['totalWithoutInsurance'] = $ret - $insuranceTotal;
}
return $ret;
}
/**
* @param TravelPeriodPrice[] $prices
* @param $persons
*
* @return array
*/
private function searchRooms($prices, $persons)
{
$ret = [];
foreach ($prices as $price)
{
$priceType = $this->priceTypeById[$price->getPriceTypeId()];
if ($priceType->getMax() == $persons['total'] &&
$priceType->getMaxAdults() >= $persons['adults'] &&
$priceType->getMinAdults() <= $persons['adults'] &&
$priceType->getMaxChildren() >= $persons['children'])
{
$ret[] = [
'priceType' => $priceType,
'persons' => $persons,
'price' => $price
];
}
}
return $ret;
}
private function splitIntoTwoGroups($prices, $persons, $mode)
{
$group1 = [];
$group2 = [];
if($mode == 'equal')
{
$group1['adults'] = $group2['adults'] = $persons['adults'] / 2;
$group1['children'] = $group2['children'] = $persons['children'] / 2;
$group1['total'] = $group2['total'] = $group1['adults'] + $group2['children'];
}
elseif($mode = 'move_without_children')
{
$group1['adults'] = $persons['adults'] - 1;
$group1['children'] = 0;
$group1['total'] = $group1['adults'] + $group1['children'];
$group2['adults'] = 1;
$group2['children'] = 0;
$group2['total'] = $group2['adults'] + $group2['children'];
}
$possibleRoomsGroup1 = $this->searchRooms($prices, $group1);
$possibleRoomsGroup2 = $this->searchRooms($prices, $group2);
return array_merge($possibleRoomsGroup1, $possibleRoomsGroup2);
}

View file

@ -7,10 +7,20 @@
namespace AppBundle\Entity;
use Symfony\Component\Validator\Constraints as Assert;
use AppBundle\Validator\Constraints as AppBundleAssert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* Class BookingRequest
* @package AppBundle\Entity
* @AppBundleAssert\BookingRequest
*/
class BookingRequest
{
// Used in SternToursCrmBookingExports, expected to be equivalent to sex (as defined in Traveler)
const MR = 1;
const MRS = 2;
/**
* @var TravelDeparturePoint $departure
*/
@ -23,14 +33,78 @@ class BookingRequest
*/
private $insurance;
private $comfort;
private $comfort = false;
private $travelOptions;
private $travelOptions = [];
private $salutation;
/**
* @Assert\NotBlank()
*/
private $firstName;
/**
* @Assert\NotBlank()
*/
private $lastName;
/**
* @Assert\NotBlank()
*/
private $streetAddress;
/**
* @Assert\NotBlank()
*/
private $zipCode;
/**
* @Assert\NotBlank()
*/
private $city;
private $nation;
/**
* @Assert\NotBlank()
*/
private $phone;
private $fax;
/**
* @Assert\NotBlank()
*/
private $email;
/*
* @ Assert\Valid()
*/
private $travelers = [];
private $notes;
/**
* @Assert\IsTrue()
*/
private $acceptTerms = false;
/**
* BookingRequest constructor.
*/
public function __construct()
{
for ($i = 0; $i < 4; ++$i)
{
$this->travelers[] = new Traveler();
}
}
/**
* @return TravelDeparturePoint
*/
public function getDeparture(): TravelDeparturePoint
public function getDeparture()
{
return $this->departure;
}
@ -62,7 +136,7 @@ class BookingRequest
/**
* @return TravelInsurance
*/
public function getInsurance(): TravelInsurance
public function getInsurance()
{
return $this->insurance;
}
@ -92,7 +166,7 @@ class BookingRequest
}
/**
* @return mixed
* @return TravelOption[]
*/
public function getTravelOptions()
{
@ -107,6 +181,221 @@ class BookingRequest
$this->travelOptions = $travelOptions;
}
/**
* @return int
*/
public function getSalutation()
{
return $this->salutation;
}
/**
* @param int $salutation
*/
public function setSalutation($salutation)
{
$this->salutation = $salutation;
}
/**
* @return string
*/
public function getFirstName()
{
return $this->firstName;
}
/**
* @param string $firstName
*/
public function setFirstName($firstName)
{
$this->firstName = $firstName;
}
/**
* @return string
*/
public function getLastName()
{
return $this->lastName;
}
/**
* @param string $lastName
*/
public function setLastName($lastName)
{
$this->lastName = $lastName;
}
/**
* @return string
*/
public function getStreetAddress()
{
return $this->streetAddress;
}
/**
* @param string $streetAddress
*/
public function setStreetAddress($streetAddress)
{
$this->streetAddress = $streetAddress;
}
/**
* @return string
*/
public function getZipCode()
{
return $this->zipCode;
}
/**
* @param string $zipCode
*/
public function setZipCode($zipCode)
{
$this->zipCode = $zipCode;
}
/**
* @return string
*/
public function getCity()
{
return $this->city;
}
/**
* @param string $city
*/
public function setCity($city)
{
$this->city = $city;
}
/**
* @return int
*/
public function getNation()
{
return $this->nation;
}
/**
* @param int $nation
*/
public function setNation($nation)
{
$this->nation = $nation;
}
/**
* @return string
*/
public function getPhone()
{
return $this->phone;
}
/**
* @param string $phone
*/
public function setPhone($phone)
{
$this->phone = $phone;
}
/**
* @return string
*/
public function getFax()
{
return $this->fax;
}
/**
* @param string $fax
*/
public function setFax($fax)
{
$this->fax = $fax;
}
/**
* @return string
*/
public function getEmail()
{
return $this->email;
}
/**
* @param string $email
*/
public function setEmail($email)
{
$this->email = $email;
}
/**
* @return Traveler[]
*/
public function getTravelers()
{
return $this->travelers;
}
/**
* @param Traveler[] $travelers
*/
public function setTravelers($travelers)
{
$this->travelers = $travelers;
}
/*
public function addTraveler(Traveler $traveler)
{
$this->travelers[] = $traveler;
}
*/
/**
* @return string
*/
public function getNotes()
{
return $this->notes;
}
/**
* @param string $notes
*/
public function setNotes($notes)
{
$this->notes = $notes;
}
/**
* @return bool
*/
public function isAcceptTerms()
{
return $this->acceptTerms;
}
/**
* @param bool $acceptTerms
*/
public function setAcceptTerms($acceptTerms)
{
$this->acceptTerms = $acceptTerms;
}
/**
* @Assert\Callback
*/

View file

@ -10,4 +10,50 @@ namespace AppBundle\Entity;
*/
class FlightPeriodRepository extends \Doctrine\ORM\EntityRepository
{
public function getIndexedFlightPeriodsForTimePeriod($startDate, $endDate, $arrivalPointIds = null)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb
->from('AppBundle:FlightPeriod', 'fp')
->addSelect('fp')
->leftJoin('fp.departures', 'fp_dep')
->addSelect('fp_dep')
;
if ($startDate !== null)
{
$qb->where('fp.startDate >= :startDate');
$qb->setParameter('startDate', $startDate);
}
if ($endDate !== null)
{
$qb->andWhere('fp.endDate <= :endDate');
$qb->setParameter('endDate', $endDate);
}
if (!empty($arrivalPointIds))
{
if (is_array($arrivalPointIds))
{
$qb->andWhere($qb->expr()->in('IDENTITY(fp.travelArrivalPoint)', $arrivalPointIds));
}
else
{
$qb->andWhere($qb->expr()->eq('IDENTITY(fp.travelArrivalPoint)', $arrivalPointIds));
}
}
$unindexedFlightPeriods = $qb->getQuery()->getResult();
$ret = [];
// Index by CONCAT(start date, end date, arrival point id):
/** @var FlightPeriod $flightPeriod */
foreach ($unindexedFlightPeriods as $flightPeriod)
{
$ret[$flightPeriod->getStartDate()->format('Y-m-d') .
$flightPeriod->getEndDate()->format('Y-m-d') .
$flightPeriod->getTravelArrivalPoint()->getId()] = $flightPeriod;
}
return $ret;
}
}

View file

@ -8,7 +8,7 @@ use Doctrine\ORM\Mapping as ORM;
* TravelBooking
*
* @ORM\Table(name="travel_booking", indexes={@ORM\Index(name="FK_travel_booking_travel_period", columns={"period_id"}), @ORM\Index(name="FK_travel_booking_travel_program", columns={"program_id"})})
* @ORM\Entity
* @ORM\Entity(repositoryClass="AppBundle\Entity\TravelBookingRepository")
*/
class TravelBooking
{
@ -678,7 +678,7 @@ class TravelBooking
*/
public function setSelectedDeparture($selectedDeparture)
{
$this->selectedDeparture = $selectedDeparture;
$this->selectedDeparture = is_array($selectedDeparture) ? json_encode($selectedDeparture) : $selectedDeparture;
return $this;
}
@ -690,7 +690,12 @@ class TravelBooking
*/
public function getSelectedDeparture()
{
return $this->selectedDeparture;
$ret = json_decode($this->selectedDeparture, true);
if (empty($ret) || !is_array($ret))
{
return $this->selectedDeparture;
}
return $ret;
}
/**
@ -846,7 +851,7 @@ class TravelBooking
*/
public function setRooms($rooms)
{
$this->rooms = $rooms;
$this->options = is_array($rooms) ? json_encode($rooms) : $rooms;
return $this;
}
@ -858,19 +863,43 @@ class TravelBooking
*/
public function getRooms()
{
return $this->rooms;
$ret = json_decode($this->rooms, true);
if (empty($ret) || !is_array($ret))
{
return $this->rooms;
}
return $ret;
}
/**
* Set participants
*
* @param string $participants
* @param Traveler[] $travelers
*
* @return TravelBooking
*/
public function setParticipants($participants)
public function setParticipants($travelers)
{
$this->participants = $participants;
if (!is_array($travelers))
{
$this->participants = $travelers;
return $this;
}
$participants = [];
for ($i = 0; $i < count($travelers); ++$i)
{
$traveler = $travelers[$i];
$participants[''. ($i+1)] = [
'gender' => $traveler->getSex(),
'first_name' => $traveler->getFirstName(),
'last_name' => $traveler->getLastName(),
'birthday' => $traveler->getBirthDate()->format('d.m.Y')
];
}
$this->participants = json_encode($participants);
return $this;
}
@ -882,7 +911,22 @@ class TravelBooking
*/
public function getParticipants()
{
return $this->participants;
$participants = json_decode($this->participants, true);
if (empty($participants) || !is_array($participants))
{
return $this->participants;
}
$ret = [];
foreach ($participants as $participant)
{
$traveler = new Traveler();
$traveler->setSex(intval($participant['gender']));
$traveler->setFirstName($participant['first_name']);
$traveler->setLastName($participant['last_name']);
$traveler->setBirthDate(\DateTime::createFromFormat('d.m.Y', $participant['birthday']));
$ret[] = $traveler;
}
return $ret;
}
/**
@ -990,7 +1034,7 @@ class TravelBooking
*/
public function setInsurances($insurances)
{
$this->insurances = $insurances;
$this->insurances = is_array($insurances) ? json_encode($insurances) : $insurances;
return $this;
}
@ -1002,7 +1046,12 @@ class TravelBooking
*/
public function getInsurances()
{
return $this->insurances;
$ret = json_decode($this->insurances, true);
if (empty($ret) || !is_array($ret))
{
return $this->insurances;
}
return $ret;
}
/**
@ -1014,7 +1063,7 @@ class TravelBooking
*/
public function setOptions($options)
{
$this->options = $options;
$this->options = is_array($options) ? json_encode($options) : $options;
return $this;
}
@ -1026,7 +1075,12 @@ class TravelBooking
*/
public function getOptions()
{
return $this->options;
$ret = json_decode($this->options, true);
if (empty($ret) || !is_array($ret))
{
return $this->options;
}
return $ret;
}
/**
@ -1038,7 +1092,7 @@ class TravelBooking
*/
public function setClassOptions($classOptions)
{
$this->classOptions = $classOptions;
$this->classOptions = is_array($classOptions) ? json_encode($classOptions) : $classOptions;
return $this;
}
@ -1050,7 +1104,12 @@ class TravelBooking
*/
public function getClassOptions()
{
return $this->classOptions;
$ret = json_decode($this->classOptions, true);
if (empty($ret) || !is_array($ret))
{
return $this->classOptions;
}
return $ret;
}
/**
@ -1062,7 +1121,7 @@ class TravelBooking
*/
public function setExtraCategory($extraCategory)
{
$this->extraCategory = $extraCategory;
$this->extraCategory = is_array($extraCategory) ? json_encode($extraCategory) : $extraCategory;
return $this;
}
@ -1074,7 +1133,12 @@ class TravelBooking
*/
public function getExtraCategory()
{
return $this->extraCategory;
$ret = json_decode($this->extraCategory, true);
if (empty($ret) || !is_array($ret))
{
return $this->extraCategory;
}
return $ret;
}
/**

View file

@ -0,0 +1,88 @@
<?php
namespace AppBundle\Entity;
/**
* TravelBookingRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class TravelBookingRepository extends \Doctrine\ORM\EntityRepository
{
public function createFromBookingRequest(BookingRequest $bookingRequest, TravelDate $travelDate, $bookingPriceInfo)
{
$tp = $travelDate->getTravelProgram();
$ret = new TravelBooking();
$ret->setIp($_SERVER['REMOTE_ADDR']);
$ret->setCreated(new \DateTime());
$ret->setProgramName($tp->getTitle() . ' ('. $travelDate->getName() .')');
//$ret->setClass()
$ret->setSalutation($bookingRequest->getSalutation());
$ret->setFirstName($bookingRequest->getFirstName());
$ret->setLastName($bookingRequest->getLastName());
$ret->setStreet($bookingRequest->getStreetAddress());
//$ret->setHouseNr()
$ret->setZipcode($bookingRequest->getZipCode());
$ret->setCity($bookingRequest->getCity());
$ret->setCountry($bookingRequest->getNation());
$ret->setMail($bookingRequest->getEmail());
$ret->setPhone($bookingRequest->getPhone());
$ret->setFax($bookingRequest->getFax());
$ret->setSelectedDeparture([
'name' => $bookingRequest->getDeparture()->getName(),
'extra_charge' => $bookingRequest->getDeparture()->getExtraCharge(),
'extra_charge_total' => $bookingRequest->getTravelerCount()
]);
$ret->setSelectedStartDate($travelDate->getStart());
$ret->setSelectedEndDate($travelDate->getEnd());
$ret->setSelectedAdults($bookingRequest->getTravelerCount());
$ret->setSelectedChild1(0);
$ret->setSelectedChild2(0);
$ret->setSelectedChild3(0);
$insurance = $bookingRequest->getInsurance();
$ret->setInsuranceName($insurance ? $insurance->getName() : '0'); // #TODO Adapted from v2
if (empty($bookingPriceInfo['insurances']))
{
$ret->setInsurances(false);
}
else
{
$insurances = [];
foreach ($bookingPriceInfo['insurances'] as $insuranceInfo)
{
$insurances[] = [
'count' => $insuranceInfo['count'],
'price' => $insuranceInfo['insurancePriceValue'],
'code' => $insuranceInfo['insurancePrice']->getCode()
];
}
$ret->setInsurances($insurances);
}
$ret->setParticipants(array_slice($bookingRequest->getTravelers(), 0, $bookingRequest->getTravelerCount()));
$ret->setParticipantsTotal($bookingRequest->getTravelerCount());
$ret->setRooms($bookingPriceInfo['rooms']);
$ret->setPriceTotal($bookingPriceInfo['total']);
$ret->setComments($bookingRequest->getNotes());
if (empty($bookingPriceInfo['options']))
{
$ret->setOptions(false);
}
else
{
$options = [];
foreach ($bookingPriceInfo['options'] as $option)
{
$options[] = [
'name' => $option->getName(),
'price' => $option->getPrice()
];
}
$ret->setOptions($options);
}
$ret->setClassOptions(false);
$ret->setExtraCategory(empty($bookingPriceInfo['classOptions']) ? false : $bookingPriceInfo['classOptions']);
return $ret;
}
}

View file

@ -89,12 +89,12 @@ final class TravelDate
}
$this->start = $start;
$this->index = $index;
$this->flightPeriod = $flightPeriod;
}
else
{
$this->start = $travelPeriod->getStartDate();
}
$this->flightPeriod = $flightPeriod;
$this->travelProgram = $travelPeriod->getProgram();
$this->key = $key;
$this->travelPeriod = $travelPeriod;
@ -187,6 +187,24 @@ final class TravelDate
return $this->departures;
}
public function getFlightPrice()
{
if ($this->travelProgram->getIsMediated())
{
return 0;
}
$flightPrice = null;
if ($this->flightPeriod !== null)
{
$flightPrice = $this->flightPeriod->getPrice();
}
if ($flightPrice === null)
{
$flightPrice = $this->travelProgram->getDefaultFlightPrice();
}
return $flightPrice;
}
/**
* @return TravelPeriodPrice[]|\Doctrine\Common\Collections\Collection
*/
@ -195,31 +213,21 @@ final class TravelDate
if (!$this->calculatedEffectivePrices)
{
$this->calculatedEffectivePrices = true;
$flightPrice = $this->getFlightPrice();
if ($this->travelProgram->getIsMediated())
{
$profitMargin = 1;
$flightPrice = 0;
}
else
{
$profitMargin = $this->travelProgram->getProfitMargin() / 100 + 1;
$flightPrice = null;
if ($this->flightPeriod !== null)
{
$flightPrice = $this->flightPeriod->getPrice();
}
if ($flightPrice === null)
{
$flightPrice = $this->travelProgram->getDefaultFlightPrice();
}
}
$currencyFactor = $this->travelProgram->getNettoPricesInEuro() ? 1 : $this->currencyFactor;
foreach ($this->travelPeriod->getPrices() as $price)
{
$price->setEffectivePrice(round(($flightPrice + $price->getPrice() * $currencyFactor) * $profitMargin));
$price->setEffectiveDiscountPrice(
round(($flightPrice + $price->getDiscountPrice() * $currencyFactor) * $profitMargin));
//$price->setEffectiveDiscountPrice($pr)
$price->setEffectiveComfortPrice(round($price->getPriceComfort() * $currencyFactor * $profitMargin));
$price->setEffectiveChildPrice(round($price->getPriceChildren() * $currencyFactor * $profitMargin));
}
}
return $this->travelPeriod->getPrices();
@ -255,6 +263,14 @@ final class TravelDate
return false;
}
/**
* @return TravelProgram
*/
public function getTravelProgram(): TravelProgram
{
return $this->travelProgram;
}
/**
* @return TravelPeriod
* @internal

View file

@ -139,11 +139,11 @@ class TravelDeparturePoint
/**
* Get extraCharge
*
* @return string
* @return float
*/
public function getExtraCharge()
{
return $this->extraCharge;
return ($this->extraCharge === null || $this->extraCharge === '') ? null : floatval($this->extraCharge);
}
/**

View file

@ -8,7 +8,7 @@ use Doctrine\ORM\Mapping as ORM;
* TravelInsurancePrice
*
* @ORM\Table(name="travel_insurance_price", indexes={@ORM\Index(name="FK_travel_insurance_price_travel_insurance", columns={"insurance_id"})})
* @ORM\Entity
* @ORM\Entity(repositoryClass="AppBundle\Entity\TravelInsurancePriceRepository")
*/
class TravelInsurancePrice
{

View file

@ -0,0 +1,32 @@
<?php
namespace AppBundle\Entity;
use Doctrine\Common\Collections\Collection;
/**
* TravelInsurancePriceRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class TravelInsurancePriceRepository extends \Doctrine\ORM\EntityRepository
{
/**
* @param $insuranceId
* @param $assessmentBasis
*
* @return TravelInsurancePrice|null
*/
public function findOneByInsuranceIdAndAssessmentBasis($insuranceId, $assessmentBasis)
{
$qb = $this->createQueryBuilder('i');
return $qb
->where($qb->expr()->eq('IDENTITY(i.insurance)', $insuranceId))
//->where('IDENTITY(i.insurance) = '. $insuranceId)
->andWhere($qb->expr()->gte('i.border', $assessmentBasis))
->orderBy('i.border')
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
}
}

View file

@ -67,7 +67,8 @@ class TravelPeriodPrice
private $priceTypeId;
private $effectivePrice = null;
private $effectiveDiscountPrice = null;
private $effectiveChildPrice = null;
private $effectiveComfortPrice = null;
/**
* Set priceType
@ -199,29 +200,28 @@ class TravelPeriodPrice
return $this->period;
}
public function getDiscountPrice()
/**
* Set priceTypeId
*
* @param integer $priceTypeId
*
* @return TravelPeriodPrice
*/
public function setPriceTypeId($priceTypeId)
{
if ($this->getPeriod() == null)
{
return null;
}
// #TODO FIX! Discount calculation differs for period and program
$price = $this->price; // #TODO Is the discount calculated for the effective price or for the original price?
$newPrice = $price;
foreach ($this->getPeriod()->getDiscounts() as $discount)
{
$newPrice -= $discount->getPercent()
? round($newPrice * $discount->getValue() / 100, 2) // FIXME
: $discount->getValue();
}
$program = $this->getPeriod()->getProgram();
if ($program != null && $program->getDiscount() != null)
{
$newPrice -= $program->getDiscountIsPercentValue()
? round($price * $program->getDiscount() / 100, 2) // FIXME
: $program->getDiscount();
}
return $price == $newPrice ? null : $newPrice;
$this->priceTypeId = $priceTypeId;
return $this;
}
/**
* Get priceTypeId
*
* @return integer
*/
public function getPriceTypeId()
{
return $this->priceTypeId;
}
/**
@ -245,48 +245,92 @@ class TravelPeriodPrice
$this->effectivePrice = $effectivePrice;
}
/**
* Probably getEffectiveDiscountPrice() is the method you are actually looking for.
* @return float|null
*/
public function getDiscountPrice()
{
return $this->calculateDiscountPrice($this->price);
}
/**
* @return float
* @throws \Exception
*/
public function getEffectiveDiscountPrice()
{
if ($this->effectiveDiscountPrice === null)
if ($this->effectivePrice === null)
{
throw new \Exception('Effective discount price must be set from outside before reading it.');
throw new \Exception('Effective price must be set from outside before reading effective discount price.');
}
return $this->effectiveDiscountPrice;
return $this->calculateDiscountPrice($this->effectivePrice);
}
/**
* @param float $effectiveDiscountPrice
* @return float
* @throws \Exception
*
* @todo The child price will not be set yet. This is just a preparation for later
*/
public function setEffectiveDiscountPrice($effectiveDiscountPrice)
public function getEffectiveChildPrice()
{
$this->effectiveDiscountPrice = $effectiveDiscountPrice;
if ($this->effectiveChildPrice === null)
{
throw new \Exception('Effective child price must be set from outside before reading it.');
}
return $this->effectiveChildPrice;
}
/**
* Set priceTypeId
*
* @param integer $priceTypeId
*
* @return TravelPeriodPrice
* @param null $effectiveChildPrice
*/
public function setPriceTypeId($priceTypeId)
public function setEffectiveChildPrice($effectiveChildPrice)
{
$this->priceTypeId = $priceTypeId;
return $this;
$this->effectiveChildPrice = $effectiveChildPrice;
}
/**
* Get priceTypeId
*
* @return integer
* @return float|null
* @throws \Exception
*/
public function getPriceTypeId()
public function getEffectiveComfortPrice()
{
return $this->priceTypeId;
if ($this->effectiveComfortPrice === null)
{
throw new \Exception('Effective comfort price must be set from outside before reading it.');
}
return $this->effectiveComfortPrice;
}
/**
* @param float|null $effectiveComfortPrice
*/
public function setEffectiveComfortPrice($effectiveComfortPrice)
{
$this->effectiveComfortPrice = $effectiveComfortPrice;
}
private function calculateDiscountPrice($price)
{
if ($this->getPeriod() == null)
{
return null;
}
$newPrice = $price;
foreach ($this->getPeriod()->getDiscounts() as $discount)
{
$newPrice -= $discount->getPercent()
? round($newPrice * $discount->getValue() / 100, 2) // #TODO FIXME
: $discount->getValue();
}
$program = $this->getPeriod()->getProgram();
if ($program != null && $program->getDiscount() != null)
{
$newPrice -= $program->getDiscountIsPercentValue()
? round($price * $program->getDiscount() / 100, 2) // #TODO FIXME
: $program->getDiscount();
}
return $price == $newPrice ? null : $newPrice;
}
}

View file

@ -129,32 +129,9 @@ class TravelPeriodRepository extends \Doctrine\ORM\EntityRepository
$usedArrivalPointIds = array_keys($isUsedArrivalPointById);
// Find flight periods and related departures
$flightPeriods = [];
if (!empty($isUsedArrivalPointById))
{
$qb = $this->getEntityManager()->createQueryBuilder();
$unindexedFlightPeriods = $qb
->from('AppBundle:FlightPeriod', 'fp')
->addSelect('fp')
->leftJoin('fp.departures', 'fp_dep')
->addSelect('fp_dep')
->where('fp.startDate >= :startDate')
->andWhere('fp.endDate <= :endDate')
->andWhere($qb->expr()->in('IDENTITY(fp.travelArrivalPoint)', $usedArrivalPointIds))
->setParameter('startDate', $startDate)
->setParameter('endDate', $endDate)
->getQuery()->getResult();
// Index by CONCAT(start date, end date, arrival point id):
/** @var FlightPeriod $flightPeriod */
foreach ($unindexedFlightPeriods as $flightPeriod)
{
$flightPeriods[$flightPeriod->getStartDate()->format('Y-m-d') .
$flightPeriod->getEndDate()->format('Y-m-d') .
$flightPeriod->getTravelArrivalPoint()->getId()] = $flightPeriod;
}
}
$flightPeriods = empty($isUsedArrivalPointById) ? []
: $this->getEntityManager()->getRepository('AppBundle:FlightPeriod')
->getIndexedFlightPeriodsForTimePeriod($startDate, $endDate, $usedArrivalPointIds);
// Find default departures and classify by-program or by-arrival-point
// We could've simply left joined them to get an equal result. But we're reducing the number of rows returned
@ -253,8 +230,9 @@ class TravelPeriodRepository extends \Doctrine\ORM\EntityRepository
// Only mediated travel programs define departures in travelPeriods
$qb->leftJoin('p.departures', 'p_dep')->addSelect('p_dep');
}
else
elseif (!($flags & self::TD_QUERY_VIRTUAL))
{
// Retrieving all flight periods by join is only possible, if virtual entries are excluded
$qb->leftJoin('AppBundle:FlightPeriod', 'fp', Expr\Join::WITH, 'IDENTITY(fp.travelArrivalPoint) = '.
':travelArrivalPointId AND d.startDate = fp.startDate AND d.endDate = fp.endDate');
$qb->setParameter('travelArrivalPointId', $program->getTravelArrivalPoint()->getId());
@ -299,11 +277,19 @@ class TravelPeriodRepository extends \Doctrine\ORM\EntityRepository
->addOrderBy('p.name', 'ASC')
;
// #TODO Try to optimize this later
$entities = $qb->getQuery()->execute();
$flightPeriodByKey = [];
$flightPeriodByKey = null;
if (!$program->getIsMediated())
{
if ($flags & self::TD_QUERY_VIRTUAL)
{
// If virtual entries are included, we have to fetch all flight periods, because we don't know
// the actual dates yet
$flightPeriodByKey = $this->getEntityManager()->getRepository('AppBundle:FlightPeriod')
->getIndexedFlightPeriodsForTimePeriod($startDate, null, $program->getTravelArrivalPoint()->getId());
}
foreach ($entities as $key => $entity)
{
if ($entity == null)
@ -334,6 +320,8 @@ class TravelPeriodRepository extends \Doctrine\ORM\EntityRepository
}
}
}
//$x = array_keys($flightPeriodByKey);
//var_dump($x); die();
$this->addTravelDatesToProgram($program, $entities, $flightPeriodByKey, $startDate, null);
return $program->getTravelDates();
@ -415,6 +403,15 @@ class TravelPeriodRepository extends \Doctrine\ORM\EntityRepository
{
$flightPeriodKey = $travelDateKey .
$travelProgram->getTravelArrivalPoint()->getId();
// #DEBUG
/*
if ($travelPeriodDate->getId() . $travelPeriod->getName() . $i == '18888D8')
{
$x = array_keys($flightPeriods);
var_dump($x);
die (isset($flightPeriods[$flightPeriodKey]) ? 'ok' : 'nok');
}
*/
$flightPeriod = $flightPeriods[$flightPeriodKey] ?? null;
}
$travelProgram->addTravelDateFromSeasonTravelPeriod(
@ -443,8 +440,13 @@ class TravelPeriodRepository extends \Doctrine\ORM\EntityRepository
$flightPeriod = null;
if (!$travelProgram->getIsMediated())
{
//$x = array_keys($flightPeriods);
//var_dump($x);
//die($travelDateKey . $travelProgram->getTravelArrivalPoint()->getId());
$flightPeriod = $flightPeriods[$travelDateKey . $travelProgram->getTravelArrivalPoint()->getId()]
?? null;
//if ($travelPeriod->getName() == 'ISRA-HOE18888D8');
//die($travelPeriod->getName() .';'. $flightPeriod->getPrice());
}
// #TODO There is an error in the old backend which causes duplicates

View file

@ -0,0 +1,104 @@
<?php
/**
* @author Ulrich Hecht <ulrich.hecht@hecht-software.de>
* @date 02/10/2017
*/
namespace AppBundle\Entity;
use Symfony\Component\Validator\Constraints as Assert;
class Traveler
{
// Used in SternToursCrmBookingExports, expected to be equivalent to salutation (as defined in BookingRequest)
const MALE = 1;
const FEMALE = 2;
/**
* @Assert\NotNull
* @ Assert\Choice(choices={1,2})
*/
private $sex;
/**
* @Assert\NotBlank()
*/
private $firstName;
/**
* @Assert\NotBlank()
*/
private $lastName;
/**
* @var \DateTime $birthDate
* @Assert\NotBlank()
*/
private $birthDate;
/**
* @return int
*/
public function getSex()
{
return $this->sex;
}
/**
* @param int $sex
*/
public function setSex($sex)
{
$this->sex = $sex;
}
/**
* @return string
*/
public function getFirstName()
{
return $this->firstName;
}
/**
* @param string $firstName
*/
public function setFirstName($firstName)
{
$this->firstName = $firstName;
}
/**
* @return string
*/
public function getLastName()
{
return $this->lastName;
}
/**
* @param string $lastName
*/
public function setLastName($lastName)
{
$this->lastName = $lastName;
}
/**
* @return \DateTime
*/
public function getBirthDate()
{
return $this->birthDate;
}
/**
* @param \DateTime $birthDate
*/
public function setBirthDate($birthDate)
{
$this->birthDate = $birthDate;
}
}

View file

@ -0,0 +1,365 @@
<?php
/**
* @author Ulrich Hecht <ulrich.hecht@hecht-software.de>
* @date 02/13/2017
*/
namespace AppBundle\Export;
use AppBundle\Entity\BookingRequest;
use AppBundle\Entity\TravelDate;
use AppBundle\Entity\Traveler;
use AppBundle\Util;
use Monolog\Logger;
class SternToursCrmBookingExporter
{
const API_URL = 'http://www.cms.stern-tours.net/api';
const API_KEY = 'f6077389c9ce710e554763a5de02c8ec';
const API_USER_ID = 15; // 'apiuser'
const WEBSITE_ID = 1; // 'sterntours.de'
private $logger;
public function __construct(Logger $logger)
{
$this->logger = $logger;
}
public function process(BookingRequest $bookingRequest, TravelDate $travelDate, $bookingPriceInfo)
{
$tp = $travelDate->getTravelProgram();
$startDateStr = $travelDate->getStart()->format('Y-m-d');
$lead = $this->createLead($bookingRequest, $travelDate);
if ($lead === null)
{
$this->warn('Failed creating lead in CRM', $bookingRequest, $travelDate, Logger::ERROR);
return false;
}
$bookingUrl = $this->createBooking($bookingRequest, $travelDate, $bookingPriceInfo, $lead['customer_id'],
$lead['id']);
if ($bookingUrl === false)
{
$this->warn('Failed creating booking in CRM', $bookingRequest, $travelDate, Logger::ERROR);
return false;
}
for ($i = 1; $i < $bookingRequest->getTravelerCount(); ++$i)
{
if (!$this->createTraveler($bookingUrl, $bookingRequest->getTravelers()[$i]))
{
$this->warn('Failed creating traveler with index '. $i .' in CRM.', $bookingRequest, $travelDate);
}
}
if ($tp->getIsMediated())
{
$serviceItemDefaults = [
'travel_company_id' => $tp->getOrganizer()->getCmsId(),
'travel_date' => $startDateStr,
'commission' => 0,
];
foreach ($bookingPriceInfo['rooms'] as $room)
{
$this->createServiceItem($bookingUrl, $serviceItemDefaults + [
'service_price' => $room['price_total'],
'name' => $room['name'],
]);
}
$this->createServiceItem($bookingUrl, $serviceItemDefaults + [
'service_price' => $bookingRequest->getTravelerCount() * $bookingPriceInfo['departure']->getExtraCharge(),
'name' => $bookingRequest->getTravelerCount() .' x '. $bookingPriceInfo['departure']->getName()
]);
foreach ($bookingRequest->getTravelOptions() as $option)
{
$this->createServiceItem($bookingUrl, $serviceItemDefaults + [
'service_price' => $option->getPrice() * $bookingRequest->getTravelerCount(),
'name' => $bookingRequest->getTravelerCount() .' x '. $option->getName()
]);
}
// Actually: extra_category
foreach ($bookingPriceInfo['classOptions'] as $classOption)
{
$this->createServiceItem($bookingUrl, $serviceItemDefaults + [
'service_price' => $classOption['count'] * $classOption['price'],
'name' => $classOption['count'] .' x '. $classOption['name']
]);
}
}
else
{
$viewPosition = 100;
$viewPositionPrice = 50;
$endDateStr = $travelDate->getEnd()->format('Y-m-d');
$arrangementDefaults = [
'state' => (new \DateTime())->format('Y-m-d'),
'in_pdf' => 1
];
$this->createArrangement($bookingUrl, $arrangementDefaults + [
'type_id' => 4, // Flug
'type_s' => 'Flug',
'begin' => $startDateStr,
'view_position' => --$viewPosition,
'data_s' => ['Hinflug' => 'von '. $bookingPriceInfo['departure']->getName()],
]);
$this->createArrangement($bookingUrl, $arrangementDefaults + [
'type_id' => 26, // Preisinformation
'type_s' => 'Preisinformation',
'view_position' => --$viewPositionPrice,
'data_s' => [
'Name' => 'Abfahrts-/Abflugort '. $bookingPriceInfo['departure']->getName(),
'Preis' => $bookingPriceInfo['departure']->getExtraCharge(),
'Teilnehmer' => $bookingRequest->getTravelerCount(),
],
]);
$this->createArrangement($bookingUrl, $arrangementDefaults + [
'type_id' => 24, // Rundreise
'type_s' => 'Rundreise', // Rundreise
'begin' => $startDateStr,
'end' => $endDateStr,
'view_position' => --$viewPosition,
'data_s' => ['Name' => $tp->getTitle() .' ('. $travelDate->getName() .')'],
]);
$roomStrs = [];
foreach ($bookingPriceInfo['rooms'] as $room)
{
$roomStrs[] = '1x '. $room['name'];
$this->createArrangement($bookingUrl, $arrangementDefaults + [
'type_id' => 26, // Preisinformation
'type_s' => 'Preisinformation',
'view_position' => --$viewPositionPrice,
'data_s' => [
'Name' => 'pro Person im \''. $room['name'] .'\'',
'Preis' => $room['price'],
'Teilnehmer' => $room['adults'],
],
]);
}
$this->createArrangement($bookingUrl, $arrangementDefaults + [
'type_id' => 5, // Hotel
'type_s' => 'Hotel',
'begin' => $startDateStr,
'end' => $endDateStr,
'view_position' => --$viewPosition,
'data_s' => ['Zimmer' => implode(', ', $roomStrs)],
]);
// Actually: extra_category
foreach ($bookingPriceInfo['classOptions'] as $classOption)
{
$this->createArrangement($bookingUrl, $arrangementDefaults + [
'type_id' => 26, // Preisinformation
'type_s' => 'Preisinformation',
'view_position' => --$viewPositionPrice,
'data_s' => [
'Name' => $classOption['name'],
'Preis' => $classOption['price'],
'Teilnehmer' => $classOption['count'],
],
]);
}
$this->createArrangement($bookingUrl, $arrangementDefaults + [
'type_id' => 4, // Flug
'type_s' => 'Flug',
'begin' => $endDateStr,
'view_position' => --$viewPosition,
'data_s' => ['Rückflug' => $bookingRequest->getDeparture()->getName()],
]);
foreach ($bookingRequest->getTravelOptions() as $option)
{
$this->createArrangement($bookingUrl, $arrangementDefaults + [
'type_id' => 26, // Preisinformation
'type_s' => 'Preisinformation',
'view_position' => --$viewPositionPrice,
'data_s' => [
'Name' => $option->getName(),
'Preis' => $option->getPrice(),
'Teilnehmer' => $bookingRequest->getTravelerCount(),
],
]);
}
}
foreach ($bookingPriceInfo['insurances'] as $insuranceInfo)
{
$this->createServiceItem($bookingUrl, [
'travel_company_id' => 30,
'service_price' => $insuranceInfo['count'] * $insuranceInfo['insurancePriceValue'],
'name' => $insuranceInfo['count'] . 'x ' . $insuranceInfo['insurance']->getName() . ' ('.
$insuranceInfo['insurancePrice']->getCode() . ')',
'commission' => round(($insuranceInfo['count'] * $insuranceInfo['insurancePriceValue']) * 20 / 100, 2),
'travel_date' => $startDateStr,
]);
}
return $bookingUrl;
}
private function createLead(BookingRequest $bookingRequest, TravelDate $travelDate)
{
$resp = $this->httpPost('lead', ['lead' => [
'customerForm' => [
'salutation_id' => $bookingRequest->getSalutation(),
'name' => $bookingRequest->getLastName(),
'firstname' => $bookingRequest->getFirstName(),
'street' => $bookingRequest->getStreetAddress(),
'zip' => $bookingRequest->getZipCode(),
'city' => $bookingRequest->getCity(),
'country_id' => $bookingRequest->getNation(),
'phone' => $bookingRequest->getPhone(),
'fax' => $bookingRequest->getFax(),
'email' => $bookingRequest->getEmail()
],
'request_date' => (new \DateTime())->format('Y-m-d'),
'sf_guard_user_id' => self::API_USER_ID,
'status_id' => 7, // 'gebucht'
'travelperiod_start' => $travelDate->getStart()->format('Y-m-d'),
'travelperiod_end' => $travelDate->getEnd()->format('Y-m-d'),
//'travelcategory_id'
'is_closed' => 1,
'website_id' => self::WEBSITE_ID,
'initialcontacttype_id' => 14,
// 'travelperiod_length
'remarks' => $bookingRequest->getNotes()
]]);
if ($resp['success'])
{
$ret = $this->httpGet($resp['location']);
if ($ret == null)
{
$this->warn('Failed retrieving newly created lead object', $bookingRequest, $travelDate);
}
return $ret;
}
return null;
}
private function createBooking(BookingRequest $bookingRequest, TravelDate $travelDate, $bookingPriceInfo,
$customerId, $leadId)
{
$tp = $travelDate->getTravelProgram();
$resp = $this->httpPost('booking', ['booking' => [
'booking_date' => (new \DateTime())->format('Y-m-d'),
'customer_id' => $customerId,
'lead_id' => $leadId,
'travel_country_id' => $tp->getTravelCountry(),
'travel_category_id' => $tp->getTravelCategory(),
'travelagenda_id' => $tp->getTravelAgenda(),
'sf_guard_user_id' => self::API_USER_ID,
'branch_id' => 4,
'website_id' => self::WEBSITE_ID,
'title' => $tp->getTitle(),
'start_date' => $travelDate->getStart()->format('Y-m-d'),
'end_date' => $travelDate->getEnd()->format('Y-m-d'),
'pax' => $bookingRequest->getTravelerCount(),
'travel_number' => $travelDate->getName(),
'price' => $bookingPriceInfo['totalWithoutInsurance'],
'participant_salutation_id' => $bookingRequest->getTravelers()[0]->getSex(),
'participant_name' => $bookingRequest->getTravelers()[0]->getLastName(),
'participant_firstname' => $bookingRequest->getTravelers()[0]->getFirstName(),
'participant_birthdate' => $bookingRequest->getTravelers()[0]->getBirthDate()->format('Y-m-d'),
]]);
if (!$resp['success'])
{
return false;
}
return $resp['location'];
}
private function createTraveler($bookingUrl, Traveler $traveler)
{
$resp = $this->httpPost($bookingUrl .'/participant.json', ['participant' => [
'participant_salutation_id' => $traveler->getSex(),
'participant_name' => $traveler->getLastName(),
'participant_firstname' => $traveler->getFirstName(),
'participant_birthdate' => $traveler->getBirthDate()->format('Y-m-d'),
]], true);
return $resp['success'];
}
private function createServiceItem($bookingUrl, $serviceItemData)
{
$resp = $this->httpPost($bookingUrl .'/serviceitem.json', ['booking_service_item' => $serviceItemData], true);
if (!$resp['success'])
{
$this->warn('Failed creating service item '. $serviceItemData['name'] .' for booking '. $bookingUrl);
}
return $resp['success'];
}
private function createArrangement($bookingUrl, $arrangementData)
{
if (isset($arrangementData['data_s']) && is_array($arrangementData['data_s']))
{
$tmp = [];
foreach ($arrangementData['data_s'] as $k => $v)
{
$tmp[] .= $k .': '. $v;
}
$arrangementData['data_s'] = implode("\n", $tmp);
}
$resp = $this->httpPost($bookingUrl .'/arrangement.json', ['arrangement' => $arrangementData], true);
if (!$resp['success'])
{
$this->warn('Failed creating arrangement item '. $arrangementData['type_s'] .' for booking '. $bookingUrl);
}
return $resp['success'];
}
private function httpGet($url)
{
$resp = Util::httpGet($url, ['X-ApiKey: '. self::API_KEY]);
$ret = json_decode($resp['content'], true);
if ($ret === null)
{
$this->warn('Invalid server response: '. $resp['content']);
}
return $ret;
}
private function httpPost($context, $postData = [], $isContextFullUrl = false)
{
$url = $isContextFullUrl ? $context : self::API_URL.'/'. $context .'.json';
$resp = Util::httpPost($url, $postData, ['X-ApiKey: '. self::API_KEY], true);
return [
'content' => json_decode($resp['content']),
'location' => isset($resp['response_headers']['location'])
? $resp['response_headers']['location']
: null,
'success' => $resp['success'] && ($resp['status_code'] == 201)
];
}
private function warn($msg, BookingRequest $bookingRequest = null, TravelDate $travelDate = null,
$level = Logger::WARNING)
{
$this->logger->log($level, 'SternToursCrmBookingExporter: '. $msg);
$this->logger->log($level, '*** Date: '. (new \DateTime())->format('d.m.Y'));
if ($travelDate !== null)
{
$this->logger->log($level, '*** Travel date: '. $travelDate->getName() .'('. $travelDate->getStart()->format('d.m.Y') .
' - '. $travelDate->getEnd()->format('d.m.Y') .')');
//$this->logger->warn('*** Travel program ID: '. $travelDate->)
}
if ($bookingRequest !== null)
{
$this->logger->log($level, '*** User name: '. $bookingRequest->getFirstName() .' '. $bookingRequest->getLastName());
}
}
}

View file

@ -7,7 +7,9 @@
namespace AppBundle\Form;
use AppBundle\Entity\BookingRequest;
use AppBundle\Entity\TravelDate;
use AppBundle\Entity\Traveler;
use AppBundle\Entity\TravelProgram;
use AppBundle\Util;
use Doctrine\Common\Collections\Collection;
@ -15,7 +17,12 @@ use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Choice;
use Symfony\Component\Validator\Constraints\NotNull;
@ -26,7 +33,20 @@ class BookingRequestType extends AbstractType
'1 Person' => 1,
'2 Personen' => 2,
'3 Personen' => 3,
'4 Personen' => 4
'4 Personen' => 4,
];
public static $NATION_CHOICES = [
'Deutschland' => 27,
'Österreich' => 34,
'Schweiz' => 181,
'Niederlande' => 196,
'Sonstiges' => 197,
];
public static $SALUTATION_CHOICES = [
'Herr' => BookingRequest::MR,
'Frau' => BookingRequest::MRS
];
/*
@ -47,7 +67,8 @@ class BookingRequestType extends AbstractType
{
$resolver->setDefaults([
'travel_date' => null,
'travel_program' => null
'travel_program' => null,
'data_class' => 'AppBundle\Entity\BookingRequest',
]);
$resolver->setAllowedTypes('travel_date', ['AppBundle\Entity\TravelDate']);
@ -65,14 +86,46 @@ class BookingRequestType extends AbstractType
/* @var TravelProgram $travelProgram */
$travelProgram = $options['travel_program'];
$builder
->add('salutation', ChoiceType::class, [
'placeholder' => 'Anrede (Bitte wählen) *',
'choices' => self::$SALUTATION_CHOICES,
'constraints' => [
new NotNull(),
new Choice(['choices' => self::$SALUTATION_CHOICES])
]
])
->add('firstName')
->add('lastName')
->add('streetAddress')
->add('zipCode')
->add('city')
->add('nation', ChoiceType::class, [
'choices' => self::$NATION_CHOICES,
'constraints' => [
new NotNull(),
new Choice(['choices' => self::$NATION_CHOICES])
]
])
->add('phone')
->add('fax')
->add('email')
->add('travelers', CollectionType::class, [
'entry_type' => TravelerType::class,
'by_reference' => false,
])
->add('notes', TextareaType::class, ['required' => false])
->add('acceptTerms', CheckboxType::class, ['required' => true])
;
$builder->add('departure', EntityType::class, [
'placeholder' => 'Abflugort (Bitte wählen) *',
'class' => 'AppBundle\Entity\TravelDeparturePoint',
'choices' => $travelDate->getDepartures(),
'constraints' => [
new NotNull(),
new Choice([
'choices' => $travelDate->getDepartures(),
'multiple' => true
'choices' => $travelDate->getDepartures()
]
)]
]);
@ -84,7 +137,6 @@ class BookingRequestType extends AbstractType
)]
]);
$insuranceChoices = [];
if ($travelProgram->getInsurance1())
{

View file

@ -0,0 +1,60 @@
<?php
/**
* @author Ulrich Hecht <ulrich.hecht@hecht-software.de>
* @date 02/10/2017
*/
namespace AppBundle\Form;
use AppBundle\Entity\Traveler;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Choice;
use Symfony\Component\Validator\Constraints\NotNull;
class TravelerType extends AbstractType
{
public static $SEX_CHOICES = [
'männlich' => Traveler::MALE,
'weiblich' => Traveler::FEMALE
];
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Traveler'
));
}
/**
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('sex', ChoiceType::class, [
'placeholder' => 'Geschlecht (Bitte wählen) *',
'choices' => self::$SEX_CHOICES,
'constraints' => [
new Choice(['choices' => self::$SEX_CHOICES])
],
'required' => true,
])
->add('firstName')
->add('lastName')
->add('birthDate', DateType::class, [
'widget' => 'single_text',
'format' => 'dd.MM.yyyy',
'required' => true,
])
;
}
}

View file

@ -71,9 +71,11 @@ class KernelControllerListener
$request->attributes->set('_controller', 'AppBundle:Default:'. $handler);
}
}
elseif (isset($parentNode) && $parentNode->getTravelProgram() != null && $pathArray[$i] == 'buchen')
elseif (isset($parentNode) && $parentNode->getTravelProgram() != null && (
$pathArray[$i] == 'buchen' || $pathArray[$i] == 'berechne-gesamtpreis'))
{
$request->attributes->set('travelProgramPage', $parentNode);
$request->attributes->set('action', $pathArray[$i]);
$request->attributes->set('_controller', 'AppBundle:Booking:index');
}
else

View file

@ -13,4 +13,9 @@
.st-booking-table .st-total-price {
border-bottom: 1px solid;
font-weight: bold;
}
/* #TODO Move */
.st-required:after {
content: " *";
}

View file

@ -733,7 +733,7 @@ a,
text-transform: uppercase;
display: block;
position: relative;
background-color: #ffc926;
background-color: #777777;
color: #fff;
padding: 8px 58px 8px 20px;
font-size: 14px;
@ -790,7 +790,7 @@ a,
z-index: 10;
left: 0;
top: 140px;
background-color: #648859;
background-color: #1a457c;
color: #fff;
padding: 4px 6px 4px 12px;
font-weight: bold;
@ -835,7 +835,7 @@ a,
text-transform: uppercase;
display: block;
position: relative;
background-color: #ffc926;
background-color: #777777;
color: #fff;
padding: 4px 12px 3px 12px;
font-size: 14px;
@ -852,18 +852,18 @@ a,
}
.item-switch > a.item-button-prev:hover {
color: #fff;
background-color: #777777;
background-color: #ffc926;
}
.item-switch > a.item-button-next:hover {
color: #fff;
background-color: #777777;
background-color: #ffc926;
}
.travel-wrapper .item > a.item-button:hover {
color: #fff;
background-color: #777777;
background-color: #ffc926;
}
.travel-wrapper .item > a.item-button:hover:after {
background-color: #5e5e5e;
background-color: #f2b600;
}
.travel-wrapper .item > a.item-button:after {
transition: 0.5s ease;
@ -873,7 +873,7 @@ a,
bottom: 0;
width: 46px;
right: 0px;
background-color: #f2b600;
background-color: #5e5e5e;
content: '';
position: absolute;
text-align: center;
@ -883,7 +883,7 @@ a,
font-size: 26px;
}
.item-switch > a.item-button-prev:hover:before {
background-color: #5e5e5e;
background-color: #f2b600;
}
.item-switch > a.item-button-prev:before {
transition: 0.5s ease;
@ -893,7 +893,7 @@ a,
bottom: 0;
width: 30px;
left: 0px;
background-color: #f2b600;
background-color: #5e5e5e;
content: '\f104';
position: absolute;
text-align: center;
@ -903,7 +903,7 @@ a,
font-size: 26px;
}
.item-switch > a.item-button-next:hover:after {
background-color: #5e5e5e;
background-color: #f2b600;
}
.item-switch > a.item-button-next:after {
transition: 0.5s ease;
@ -913,7 +913,7 @@ a,
bottom: 0;
width: 30px;
right: 0px;
background-color: #f2b600;
background-color: #5e5e5e;
content: '';
position: absolute;
text-align: center;
@ -3296,6 +3296,11 @@ h5:hover a,
.c2 li span,
.btn-primary {
color: #fff;
background-color: #777777;
border-color: #777777;
}
.btn-primary:hover,
.btn-primary:active:hover {
background-color: #ffc926;
border-color: #ffc926;
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,45 @@
$(document).ready(function() {
var frm$ = $('.st-booking-form');
var summary$ = $('.st-booking-summary');
var travelerCountDd$ = $('#booking_request_travelerCount');
var travelers$ = $('.st-traveler');
var travelerFields$ = travelers$.find('input,select');
frm$.find('input, select').change(function() {
var tmp = location.href.split('?');
var tmp2 = tmp[0].split('/');
tmp2.pop();
var url = tmp2.join('/') + '/berechne-gesamtpreis';
if (tmp[1])
{
url += '?'+ tmp[1];
}
$.ajax({
url: url,
type: 'post',
data: frm$.serialize()
}).then(function(r) {
summary$.html(r);
}, function() {
summary$.html('Aufgrund eines Fehlers konnte kein Angebot ermittelt werden.');
})
});
function updateTravelers()
{
var travelerCount = parseInt(travelerCountDd$.val());
travelers$.hide();
travelerFields$.prop('required', false);
for (var i = 1; i <= travelerCount; ++i)
{
$('.st-traveler-'+ i).show().find('input,select').prop('required', true);
}
}
travelerCountDd$.change(updateTravelers);
updateTravelers();
});

View file

@ -0,0 +1,72 @@
<?php
/**
* @author Ulrich Hecht <ulrich.hecht@hecht-software.de>
* @date 02/10/2017
*/
namespace AppBundle\Twig;
class AppExtension extends \Twig_Extension
{
protected $environment;
private $template;
public function __construct(\Twig_Environment $env)
{
$this->environment = $env;
}
public function getFunctions()
{
return array(
'form_field' => new \Twig_SimpleFunction('form_field', array($this, 'formField'), array(
'is_safe' => array('html')
)),
'form_field_pho' => new \Twig_SimpleFunction('form_field_pho', array($this, 'formFieldPho'), array(
'is_safe' => array('html')
)),
);
}
public function formField($form, $label = null, $opt = null)
{
$this->template = $this->environment->loadTemplate( '::default/form/helpers.html.twig' );
return $this->template->renderBlock('form_field', array(
'form' => $form,
'label' => $label,
'opt' => $opt
));
}
/**
* Form field with placeholder only
*
* @param $form
* @param null $label
* @param null $opt
*
* @return mixed
*/
public function formFieldPho($form, $label = null, $opt = [])
{
$this->template = $this->environment->loadTemplate( '::default/form/helpers.html.twig' );
return $this->template->renderBlock('form_field_pho', array(
'form' => $form,
'label' => $label,
'opt' => $opt
));
}
/**
* Returns the name of the extension.
*
* @return string The extension name
*/
public function getName()
{
return 'app_extension';
}
}

View file

@ -35,6 +35,108 @@ class Util
$prop->setValue($entity, $collection);
}
public static function formatPrice($value)
{
return number_format($value, 2, ',', '.') .' €';
}
static function httpRequest($url, $method = "GET", $data = "", $headers = array(), $withRespHeaders = false,
$cookieJar = null)
{
global $kernel;
$ch = curl_init();
$headerList = array();
//self::buildHttpQueryForCurl($headers, $headerList); //?
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_HEADER, $withRespHeaders);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if (isset($cookieJar))
{
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookieJar);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookieJar);
}
if (strtoupper($method) == 'POST')
{
$dataStr = is_array($data) ? http_build_query($data) : $data;
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $dataStr);
}
$response = curl_exec($ch);
$ret = [
'info' => curl_getinfo($ch),
'success' => true,
'status_code' => curl_getinfo($ch, CURLINFO_HTTP_CODE)
];
if ($withRespHeaders)
{
$respArray = explode("\r\n\r\n", $response);
$ret['response_headers'] = [];
// count() - 2 due to HTTP status 100 (continue) with multiple responses
$headers = is_array($respArray) && isset($respArray[count($respArray) - 2])
? explode("\r\n", $respArray[count($respArray) - 2])
: [];
// HTTP/1.1 200 OK
array_shift($headers);
foreach ($headers as $header)
{
$headerKV = explode(': ', $header);
$key = strtolower($headerKV[0]);
if (isset($ret['response_headers'][$key]))
{
if (is_array($ret['response_headers'][$key]))
{
$ret['response_headers'][$key][] = $headerKV[1];
}
else
{
$ret['response_headers'][$key] = array($ret['response_headers'][$key], $headerKV[1]);
}
}
else
{
$ret['response_headers'][$key] = $headerKV[1];
}
}
$ret['content'] = $respArray[count($respArray) - 1];
}
else
{
$ret['content'] = $response;
}
if (curl_errno($ch) > 0)
{
$ret['info']['curl_errno'] = curl_errno($ch);
$ret['success'] = false;
}
if (isset($kernel))
{
$logger = $kernel->getContainer()->get('logger');
$logger->warn('HTTP request to \''. $url .'\' with server response code '. $ret['status_code']);
}
curl_close($ch);
return $ret;
}
static function httpPost($url, $postData = '', $headers = array(), $withRespHeaders = true, $cookieJarPath = null)
{
return self::httpRequest($url, 'POST', $postData, $headers, $withRespHeaders, $cookieJarPath);
}
static function httpGet($url, $headers = array(), $withRespHeaders = false, $cookieJarPath = null)
{
return self::httpRequest($url, 'GET', '', $headers, $withRespHeaders, $cookieJarPath);
}
/**
* Prints formatted back-trace. CLI-output is colorized as well.
*