Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow $0 trip fees #4499

Merged
merged 5 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/Http/Controllers/SquareCheckoutController.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ public function payTravel(Request $request)
->whereHas(
'travel',
static function (Builder $query): void {
$query->whereIn('status', ['approved', 'complete']);
$query->whereIn('status', ['approved', 'complete'])
->where('fee_amount', '>', 0);
}
)
->unpaid()
Expand Down
20 changes: 19 additions & 1 deletion app/Mail/Travel/TravelAssignmentCreated.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,25 @@ public function build(): self

private function subjectLineCallToAction(): string
{
if ($this->assignment->needs_docusign || ! $this->assignment->user->has_emergency_contact_information) {
if (
$this->assignment->needs_docusign &&
$this->assignment->user->has_emergency_contact_information &&
$this->assignment->travel->fee_amount === 0
) {
if ($this->assignment->travel->needs_airfare_form) {
if ($this->assignment->travel->needs_travel_information_form) {
return 'Forms';
} else {
return 'Airfare request form';
}
} else {
if ($this->assignment->travel->needs_travel_information_form) {
return 'Travel information form';
} else {
return 'Form';
}
}
} elseif ($this->assignment->needs_docusign || ! $this->assignment->user->has_emergency_contact_information) {
return 'Action';
} else {
return 'Payment';
Expand Down
3 changes: 2 additions & 1 deletion app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -1084,7 +1084,8 @@ public function getCurrentTravelAssignmentAttribute(): ?TravelAssignment
->whereHas(
'travel',
static function (Builder $query): void {
$query->whereIn('status', ['approved', 'complete']);
$query->whereIn('status', ['approved', 'complete'])
->where('fee_amount', '>', 0);
}
)
->unpaid()
Expand Down
41 changes: 28 additions & 13 deletions app/Nova/Actions/ReviewTrip.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,35 @@ public function handle(ActionFields $fields, Collection $models): ActionResponse
$trip->save();

if ($trip->needs_docusign) {
$trip->assignments->each(static function (TravelAssignment $assignment): void {
PrefetchSquareCheckoutLinkForTravelAssignment::dispatch($assignment)
->chain([
new SendDocuSignEnvelopeForTravelAssignment($assignment, true),
new SendTravelAssignmentCreatedNotification($assignment),
]);
});
if ($trip->fee_amount > 0) {
$trip->assignments->each(static function (TravelAssignment $assignment): void {
PrefetchSquareCheckoutLinkForTravelAssignment::dispatch($assignment)
->chain([
new SendDocuSignEnvelopeForTravelAssignment($assignment, true),
new SendTravelAssignmentCreatedNotification($assignment),
]);
});
} else {
$trip->assignments->each(static function (TravelAssignment $assignment): void {
SendDocuSignEnvelopeForTravelAssignment::dispatch($assignment, true)
->chain([
new SendTravelAssignmentCreatedNotification($assignment),
]);
});
}
} else {
$trip->assignments->each(static function (TravelAssignment $assignment): void {
PrefetchSquareCheckoutLinkForTravelAssignment::dispatch($assignment)
->chain([
new SendTravelAssignmentCreatedNotification($assignment),
]);
});
if ($trip->fee_amount > 0) {
$trip->assignments->each(static function (TravelAssignment $assignment): void {
PrefetchSquareCheckoutLinkForTravelAssignment::dispatch($assignment)
->chain([
new SendTravelAssignmentCreatedNotification($assignment),
]);
});
} else {
$trip->assignments->each(static function (TravelAssignment $assignment): void {
SendTravelAssignmentCreatedNotification::dispatch($assignment);
});
}
}

$trip->primaryContact->notify(new TravelApproved($trip));
Expand Down
45 changes: 2 additions & 43 deletions app/Nova/Travel.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
use App\Rules\MatrixItineraryBusinessPolicy;
use App\Util\DepartmentNumbers;
use App\Util\DocuSign;
use App\Util\Matrix;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
Expand Down Expand Up @@ -235,10 +234,10 @@ public function fields(Request $request): array
->rules(
'required',
'integer',
'min:'.config('travelpolicy.minimum_trip_fee'),
'min:0',
'max:'.config('travelpolicy.maximum_trip_fee')
)
->min(config('travelpolicy.minimum_trip_fee'))
->min(0)
->max(config('travelpolicy.maximum_trip_fee')),

Currency::make(
Expand Down Expand Up @@ -845,46 +844,6 @@ protected static function afterValidation(NovaRequest $request, $validator): voi
);
}
}

$totalCost = intval($request->tar_lodging) +
intval($request->tar_registration) +
intval($request->car_rental_cost) +
intval($request->meal_per_diem);

if ($request->resourceId !== null) {
$trip = \App\Models\Travel::where('id', '=', $request->resourceId)->sole();

$airfareCost = $trip->assignments->reduce(
static function (?float $carry, \App\Models\TravelAssignment $assignment): ?float {
$thisAirfareCost = Matrix::getHighestDisplayPrice($assignment->matrix_itinerary);

if ($thisAirfareCost !== null && $carry !== null && $thisAirfareCost > $carry) {
return $thisAirfareCost;
} elseif ($thisAirfareCost !== null && $carry === null) {
return $thisAirfareCost;
} else {
return $carry;
}
}
);

if ($airfareCost !== null && $airfareCost > 0) {
$totalCost += $airfareCost;
}
}

$feeAmount = intval($request->fee_amount);

if ($totalCost === 0) {
return;
}

if ($feeAmount / $totalCost < config('travelpolicy.minimum_trip_fee_cost_ratio')) {
$validator->errors()->add(
'fee_amount',
trim(view('nova.help.travel.feevalidation', ['totalCost' => $totalCost])->render())
);
}
}

/**
Expand Down
22 changes: 1 addition & 21 deletions app/Nova/TravelAssignment.php
Original file line number Diff line number Diff line change
Expand Up @@ -230,33 +230,13 @@ protected static function afterValidation(NovaRequest $request, $validator): voi

$businessPolicy = new MatrixItineraryBusinessPolicy($trip->airfare_policy);

$businessPolicyPassed = true;

$businessPolicy->validate(
'matrix_itinerary',
$request->matrix_itinerary,
static function (string $message) use ($validator, &$businessPolicyPassed): void {
$businessPolicyPassed = false;
static function (string $message) use ($validator): void {
$validator->errors()->add('matrix_itinerary', $message);
}
);

if ($businessPolicyPassed) {
$airfare_cost = Matrix::getHighestDisplayPrice($request->matrix_itinerary);

if ($airfare_cost === null) {
$validator->errors()->add('matrix_itinerary', 'Internal error determining price for itinerary');
}

$total_cost = $trip->tar_lodging + $trip->tar_registration + $airfare_cost;

if ($trip->fee_amount / $total_cost < config('travelpolicy.minimum_trip_fee_cost_ratio')) {
$validator->errors()->add(
'matrix_itinerary',
trim(view('nova.help.travel.assignment.feevalidation', ['totalCost' => $total_cost])->render())
);
}
}
}

/**
Expand Down
4 changes: 0 additions & 4 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,6 @@ parameters:
- '#Only booleans are allowed in a negated boolean, Illuminate\\Support\\Carbon\|null given\.#'
- '#Only booleans are allowed in a negated boolean, mixed given\.#'
- '#Only booleans are allowed in a ternary operator condition, Illuminate\\Support\\Carbon\|null given\.#'
- '#Only numeric types are allowed in \+, float\|null given on the right side\.#'
- '#Only numeric types are allowed in \+, int\|null given on the left side\.#'
- '#Only numeric types are allowed in \+, int\|null given on the right side\.#'
- '#Parameter \#1 ...\$addresses of method Symfony\\Component\\Mime\\Email::replyTo\(\) expects string\|Symfony\\Component\\Mime\\Address, mixed given\.#'
- '#Parameter \#1 \$[a-z_]+ of method DocuSign\\eSign\\Model\\[a-zA-Z]+::set[a-zA-Z]+\(\) expects string\|null, false given\.#'
- '#Parameter \#1 \$[a-z_]+ of method DocuSign\\eSign\\Model\\[a-zA-Z]+::set[a-zA-Z]+\(\) expects string\|null, int given\.#'
Expand Down Expand Up @@ -166,7 +163,6 @@ parameters:
- '#Parameter \#1 \$id of static method Illuminate\\Database\\Eloquent\\Builder<App\\Models\\User>::findByIdentifier\(\) expects string, mixed given\.#'
- '#Parameter \#1 \$idempotencyKey of static method Square\\Models\\Builders\\RefundPaymentRequestBuilder::init\(\) expects string, string\|null given\.#'
- '#Parameter \#1 \$ids of method Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany<Illuminate\\Database\\Eloquent\\Model>::sync\(\) expects array\|Illuminate\\Database\\Eloquent\\Model\|Illuminate\\Support\\Collection, mixed given\.#'
- '#Parameter \#1 \$itinerary of static method App\\Util\\Matrix::getHighestDisplayPrice\(\) expects array\|string\|null, mixed given\.#'
- '#Parameter \#1 \$json of function json_decode expects string, mixed given\.#'
- '#Parameter \#1 \$json of function json_decode expects string, string\|false given\.#'
- '#Parameter \#1 \$locationId of class Square\\Models\\Order constructor expects string, mixed given\.#'
Expand Down
6 changes: 5 additions & 1 deletion resources/views/mail/travel/assignmentcreated.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@
You have been assigned to {{ $assignment->travel->name }}. Complete the following tasks as soon as possible so that we can book travel arrangements for you.

Visit {{ route('docusign.travel') }} to submit {{ $assignment->travel->needs_airfare_form ? ($assignment->travel->needs_travel_information_form ? 'forms' : 'an airfare request form') : ($assignment->travel->needs_travel_information_form ? 'a travel information form' : '') }} for your trip.
@if($assignment->travel->fee_amount > 0)

Make a ${{ intval($assignment->travel->fee_amount) }} payment for the trip fee. You can pay online with a credit or debit card at {{ route('pay.travel') }}.
@else
@endif
@elseif($assignment->travel->fee_amount > 0)
You have been assigned to {{ $assignment->travel->name }}. Pay the ${{ intval($assignment->travel->fee_amount) }} trip fee as soon as possible{{ $assignment->travel->return_date > \Carbon\Carbon::now() ? ' so that we can book travel arrangements for you' : '' }}. You can pay online with a credit or debit card at {{ route('pay.travel') }}.
@endif
@if($assignment->travel->fee_amount > 0)

If you would prefer to pay by cash or check, make arrangements with {!! $assignment->travel->primaryContact->full_name !!}. Write checks to Georgia Tech, with RoboJackets on the memo line. Don't forget to sign it!
@endif
@if(!$assignment->user->has_emergency_contact_information && $assignment->travel->return_date > \Carbon\Carbon::now())

You also need to add emergency contact information to your {{ config('app.name') }} profile at {{ route ('profile') }}.
Expand Down
2 changes: 1 addition & 1 deletion resources/views/nova/help/travel/feeamount.blade.php
Original file line number Diff line number Diff line change
@@ -1 +1 @@
This is the amount that will be collected from <strong>each</strong> traveler. The trip fee must be at least {{ (config('travelpolicy.minimum_trip_fee_cost_ratio') * 100) }}% of the per-person cost for this trip. Online payments incur a <a href="https://squareup.com/us/en/payments/our-fees">~4% processing fee</a> paid by RoboJackets.
This is the amount that will be collected from <strong>each</strong> traveler. Online payments incur a <a href="https://squareup.com/us/en/payments/our-fees">~4% processing fee</a> paid by RoboJackets.
56 changes: 56 additions & 0 deletions routes/mailbook.php
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,62 @@
Mailbook::to($user)
->add(TravelAssignmentCreated::class)
->label('Trip Assignment Created')
->variant('Need Travel Information Form No Payment', static function (): TravelAssignmentCreated {
$user = User::withoutEvents(static function (): User {
$user = User::factory()->make([
'first_name' => 'George',
'preferred_name' => null,
'last_name' => 'Burdell',
'gt_email' => '[email protected]',
'primary_affiliation' => 'student',
'emergency_contact_name' => 'asdf',
'emergency_contact_phone' => 'asdf',
]);
$user->save();

return $user;
});

$officer = User::withoutEvents(static function (): User {
$officer = User::factory()->make([
'first_name' => 'Robo',
'preferred_name' => null,
'last_name' => 'Buzz',
'gt_email' => '[email protected]',
]);
$officer->save();

return $officer;
});

$travel = Travel::withoutEvents(static fn (): Travel => Travel::firstOrCreate([
'name' => 'Motorama 2022',
], [
'destination' => 'mailbook',
'departure_date' => '2022-02-18',
'return_date' => '2022-02-21',
'fee_amount' => 0,
'forms' => [
Travel::TRAVEL_INFORMATION_FORM_KEY => true,
],
'primary_contact_user_id' => $officer->id,
'included_with_fee' => 'mailbook',
'is_international' => false,
'status' => 'draft',
]));

$assignment = TravelAssignment::withoutEvents(static function () use ($user, $travel): TravelAssignment {
$assignment = TravelAssignment::factory()->make([
'user_id' => $user->id,
'travel_id' => $travel->id,
]);
$assignment->save();

return $assignment;
});

return new TravelAssignmentCreated($assignment);
})
->variant('Need Travel Information Form', static function (): TravelAssignmentCreated {
$user = User::withoutEvents(static function (): User {
$user = User::factory()->make([
Expand Down