Compare commits

..

169 commits

Author SHA1 Message Date
Ramon Gutierrez
630796df85 Merge branch '811-add-sealant-fee' into 'master'
Resolve "Add sealant fee"

Closes #811

See merge request jankstudio/resq!924
2024-12-08 14:35:58 +00:00
dedfb58a28 Fix issue of sealant and coolant fee not displaying in open edit form. #811 2024-12-05 01:48:39 -05:00
1b24e68f6b Add processing of sealant fee for tire repair service. #811 2024-12-04 02:51:50 -05:00
Ramon Gutierrez
dc4d5f90a7 Merge branch '808-add-form-validation-to-role' into 'master'
Validate role ID and name are not blank #808

Closes #808

See merge request jankstudio/resq!923
2024-10-02 05:58:32 +00:00
Ramon Gutierrez
0179a1b87e Validate role ID and name are not blank #808 2024-10-02 13:58:03 +08:00
Ramon Gutierrez
f1609b2273 Merge branch '807-disable-sms-for-certain-rejection-reasons' into 'master'
Resolve "Disable SMS for certain Rejection Reasons"

Closes #807 and #792

See merge request jankstudio/resq!922
2024-09-24 07:15:30 +00:00
22215ff9f6 Merge branch '807-disable-sms-for-certain-rejection-reasons' of gitlab.com:jankstudio/resq into 807-disable-sms-for-certain-rejection-reasons 2024-09-24 02:47:11 -04:00
Ramon Gutierrez
6994d69020 Merge branch '792-battery-facilitated-by-issue' into 'master'
Resolve "Battery Facilitated By issue"

Closes #792

See merge request jankstudio/resq!903
2024-09-24 02:45:43 -04:00
Ramon Gutierrez
83bdb5c626 Merge branch '792-battery-facilitated-by-issue' into 'master'
Resolve "Battery Facilitated By issue"

Closes #792

See merge request jankstudio/resq!903
2024-09-24 04:40:00 +00:00
Ramon Gutierrez
5a5113b166 Merge branch '803-fix-invoice-data-disappearing-on-open-edit-form' into 'master'
Fix invoice data on open edit form

Closes #803

See merge request jankstudio/resq!918
2024-09-24 04:23:02 +00:00
Ramon Gutierrez
629cc2afa1 Merge branch 'master' into 803-fix-invoice-data-disappearing-on-open-edit-form 2024-07-30 21:27:23 +08:00
Ramon Gutierrez
ad946247ba Merge branch '806-restore-inventory-counters-on-processing-form' into 'master'
Restore inventory retrieval on processing form #806

Closes #806

See merge request jankstudio/resq!921
2024-07-30 13:26:45 +00:00
Ramon Gutierrez
63902e5838 Restore inventory retrieval on processing form #806 2024-07-30 16:39:11 +08:00
Ramon Gutierrez
511aa5ec9b Merge branch 'master' into 803-fix-invoice-data-disappearing-on-open-edit-form 2024-07-18 16:34:59 +08:00
Ramon Gutierrez
2df53674c4 Merge branch '800-automatically-reject-jo-if-no-riders-or-inventory' into 'master'
Add hub selector filtering for available riders, refactor inventory filter to...

Closes #800

See merge request jankstudio/resq!920
2024-07-18 08:34:02 +00:00
Ramon Gutierrez
4ab6b0659d Merge branch 'master' into 800-automatically-reject-jo-if-no-riders-or-inventory 2024-07-18 16:30:10 +08:00
Ramon Gutierrez
a66d16a58b Restored hub filter area functionality #800 2024-07-16 16:10:29 +08:00
Ramon Gutierrez
5f2ed34d8c Disable round robin filter for emergency JOs #800 2024-07-12 14:17:59 +08:00
Ramon Gutierrez
e7d8af516e Temporarily disable hub filter area checks #800 2024-07-09 16:55:58 +08:00
Ramon Gutierrez
8da5381a1c Enable inventory retrieval if inventory checks are disabled #800 2024-07-05 15:54:14 +08:00
Ramon Gutierrez
f8d90cbdcd Fix typo on ResqJobOrderHandler properties #800 2024-07-04 17:06:46 +08:00
Ramon Gutierrez
6139e649fb Add logging for closest hubs selected #800 2024-07-04 16:10:02 +08:00
Ramon Gutierrez
e1103cf108 Move enabled filters list to env #800 2024-07-04 15:41:01 +08:00
Ramon Gutierrez
a10f58e425 Add filtering for JOs with VIP transaction origin #800 2024-07-04 12:57:27 +08:00
Ramon Gutierrez
23d814d1e9 Add extra logs to hub filtering #800 2024-07-03 23:41:44 +08:00
Ramon Gutierrez
9b63139957 Fix file logging format for hub filtering #800 2024-07-03 23:14:38 +08:00
Ramon Gutierrez
a335f84a13 Fix hub filter log location #800 2024-07-03 14:01:16 +08:00
Ramon Gutierrez
8992fdeec0 Add hub rejection log file #800 2024-07-03 13:56:27 +08:00
Ramon Gutierrez
57b4227da9 Add more requested JO rejection reasons #800 2024-06-30 11:47:23 +08:00
Ramon Gutierrez
bff1f7ff73 Add null check to battery inventory entries from motiv #800 2024-06-27 11:31:02 +08:00
Ramon Gutierrez
e8b14ff379 Fix available rider filter to also consider flag_active and current_job_order #800 2024-06-27 10:58:11 +08:00
Ramon Gutierrez
44b3753ece Skip hub filtering if hub list is alraedy empty #800 2024-06-26 10:02:04 +08:00
Ramon Gutierrez
a21ed66490 Move requested params to filters instead of hub selector #800 2024-06-26 09:59:29 +08:00
3fc04cd733 Fix trade-in issue for invoice. #803 2024-06-25 01:44:44 -04:00
Ramon Gutierrez
3b287236ec Add regional filter support for hub filters #800 2024-06-25 03:38:56 +08:00
Ramon Gutierrez
204c039fba Refactor hub selector to separate filters into individual classes #800 2024-06-25 03:19:10 +08:00
Ramon Gutierrez
b4057de938 Add hub filter exceptions to supported areas #800 2024-06-25 03:18:31 +08:00
2a30787dba Fix for issue with customer app JO request with trade in. #803 2024-06-24 06:00:01 -04:00
acee5f8f55 Populate invoice items array upon loading of form. #803 2024-06-21 05:37:04 -04:00
90628f0b54 Add trade in type to invoice item. #03 2024-06-21 05:24:58 -04:00
4ccdd29ae6 Changed the input for trade in items to battery size. #03 2024-06-21 03:39:44 -04:00
2f9ee4ec67 Add battery to criteria for battery replacement warranty. #803 2024-06-20 05:26:23 -04:00
4b5ad97225 Add battery size id to invoice item. Add saving of trade in items with battery size to invoice item. #803 2024-06-20 03:29:24 -04:00
Ramon Gutierrez
e83de164c1 Merge branch '804-add-checking-for-null-user-for-rejection-detail-report' into 'master'
Resolve "Add checking for null user for rejection detail report"

Closes #804

See merge request jankstudio/resq!917
2024-06-10 10:40:38 +00:00
8c886c4dbf Add check for null for user for hub rejection. #804 2024-06-10 02:20:28 -04:00
Ramon Gutierrez
10c44fbe64 Limit hub rejection SMS to one per JO/hub combo #800 2024-06-07 04:19:08 +08:00
Ramon Gutierrez
b5f169c14e Merge branch 'master' into 800-automatically-reject-jo-if-no-riders-or-inventory 2024-06-06 05:52:03 +08:00
Ramon Gutierrez
cec648f894 Merge branch '801-handle-failed-paymongo-payments' into 'master'
Attempt to re-enable paymongo webhook if found disabled #801

Closes #801

See merge request jankstudio/resq!916
2024-06-05 21:51:18 +00:00
Ramon Gutierrez
c4b0513386 Attempt to re-enable paymongo webhook if found disabled #801 2024-06-06 05:50:36 +08:00
Ramon Gutierrez
90838b004a Merge branch 'master' into 800-automatically-reject-jo-if-no-riders-or-inventory 2024-06-04 17:49:39 +08:00
Ramon Gutierrez
c60a39c13e Merge branch '802-fix-jos-being-able-to-proceed-after-cancellation' into 'master'
Add checks to rider app API for JO status and delivery status #802

Closes #802

See merge request jankstudio/resq!915
2024-06-04 09:47:46 +00:00
Ramon Gutierrez
792db080a1 Merge branch '801-handle-failed-paymongo-payments' into 'master'
Resolve "Handle failed PayMongo payments"

Closes #801

See merge request jankstudio/resq!914
2024-06-04 09:45:37 +00:00
Ramon Gutierrez
08084f682c Add force option to enable or disable webhook status checking, defaults to false #801 2024-05-31 18:51:55 +08:00
Ramon Gutierrez
3846ad5a43 Add checker for paymongo webhook status before running manual processing command #801 2024-05-31 18:45:16 +08:00
Ramon Gutierrez
191a02f4c4 Fix typo on getStatus call on entity listener #801 2024-05-31 12:28:43 +08:00
Ramon Gutierrez
e97cebd2b2 Fix typo in log message for new command #801 2024-05-31 12:11:13 +08:00
Ramon Gutierrez
952122a39e Prevent insurance applications from being flagged as paid more than once #801 2024-05-31 06:50:34 +08:00
Ramon Gutierrez
ad841a7e25 Prevent success handler from catching failed paymongo payment attempts #801 2024-05-31 06:49:55 +08:00
Ramon Gutierrez
75b2ada03f Add command for processing late pending paymongo transactions #801 2024-05-31 06:48:44 +08:00
Ramon Gutierrez
4b19cff996 Add checks to rider app API for JO status and delivery status #802 2024-05-30 17:15:47 +08:00
Ramon Gutierrez
2ebb6e040a Add placeholder debug logs #800 2024-05-30 16:06:55 +08:00
Ramon Gutierrez
404401d854 Fix erroneous delivery status IDs on collection #800 2024-05-30 16:06:04 +08:00
Ramon Gutierrez
6dfaeee799 Generate JO ID before hub selection on user app API, add additional params to HubCriteria for JORejection and SMS #800 2024-05-20 03:46:08 +08:00
Ramon Gutierrez
aae4aaa390 Enable inventory and rider hub filters for emergency JOs, disable for VIP customers #800 2024-05-18 15:21:32 +08:00
Ramon Gutierrez
b53aacb840 Update rejection messages to use battery name and model vs SKUs #800 2024-05-17 22:23:26 +08:00
Ramon Gutierrez
22683e1edf Add null check to user on detailed rejection report #800 2024-05-17 22:01:43 +08:00
Ramon Gutierrez
4be9134090 Update SMS message format for rejected hubs, add JO rejection entries for no inventory or no riders #800 2024-05-17 21:40:16 +08:00
Ramon Gutierrez
21c97df677 Set JO type regardless of area support on hub form initialization to trigger inventory filter #800 2024-05-16 21:46:02 +08:00
Ramon Gutierrez
5cf2c3619f Remove debug code from JO handler #800 2024-05-16 15:07:35 +08:00
Ramon Gutierrez
7b6afbb099 Modify inventory SMS message #800 2024-05-15 18:00:27 +08:00
Ramon Gutierrez
190ac88153 Enable inventory and rider filtering on CRM JO form hub lists #800 2024-05-15 18:00:07 +08:00
Ramon Gutierrez
aed31f2a33 Rearrange flow logic on hub selector, save inventory counts upon retrieval if enabled #800 2024-05-15 17:59:27 +08:00
Ramon Gutierrez
76496ed6fe Fix issues with inventory filter flow #800 2024-05-14 17:51:00 +08:00
Ramon Gutierrez
89e5dd799f Update hub rejection SMS translations #800 2024-05-14 17:49:34 +08:00
Ramon Gutierrez
0bd6a89840 Fix missing keys on NameValue class #800 2024-05-14 17:49:14 +08:00
Ramon Gutierrez
be0e69db89 Add blacklist support to base NameValue class, for hiding options when getting entire collection #800 2024-05-14 14:45:51 +08:00
Ramon Gutierrez
d52402a2ef Enable inventory and rider checks on hub criteria on user app and TAPI #800 2024-05-14 04:18:37 +08:00
Ramon Gutierrez
a45c3dd65c Add hub selector filtering for available riders, refactor inventory filter to send a single batch call instead #800 2024-05-14 03:39:45 +08:00
Ramon Gutierrez
b19d9c203a Merge branch '783-rider-app-trade-in-support' into 'master'
Resolve "Rider app trade-in support"

Closes #783

See merge request jankstudio/resq!912
2024-04-23 13:11:51 +00:00
Ramon Gutierrez
a64557ffcd Fix API roles permissions list to properly display the correct ACL group #783 2024-04-06 14:56:16 +08:00
Ramon Gutierrez
d7909f7941 Merge branch '796-price-display-issue-with-price-tier' into '783-rider-app-trade-in-support'
Resolve "Price display issue with price tier"

See merge request jankstudio/resq!908
2024-04-05 10:59:32 +00:00
Ramon Gutierrez
8e383fd23f Merge branch '794-capi-calls-for-pullout-form-system' into '783-rider-app-trade-in-support'
Resolve "CAPI calls for Pullout Form System"

See merge request jankstudio/resq!906
2024-04-05 10:57:13 +00:00
Ramon Gutierrez
86889b3147 Merge branch '798-motolite-user-jumpstart-fee' into '783-rider-app-trade-in-support'
Resolve "Motolite user jumpstart fee"

See merge request jankstudio/resq!910
2024-04-02 21:08:35 +00:00
6a804c11df Add sql file to update service offering table. #798 2024-04-02 01:21:23 -04:00
c8e2c02be1 Add a separate service fee for motolite users for jumpstart. #798 2024-04-02 01:11:32 -04:00
Ramon Gutierrez
06dc8eae7b Merge branch '795-jumpstart-service-fee-for-motolite-users' into '783-rider-app-trade-in-support'
Resolve "Jumpstart service fee for motolite users"

See merge request jankstudio/resq!907
2024-04-01 13:19:07 +00:00
Ramon Gutierrez
f97f287f5b Merge branch '797-resq-dispatch-display-issue' into '783-rider-app-trade-in-support'
Resolve "Resq Dispatch Display Issue"

See merge request jankstudio/resq!909
2024-04-01 13:17:25 +00:00
6f2fca292e Add use statement. #797 2024-04-01 06:55:23 -04:00
b80ece9084 Fix display issue for invoice. #796 2024-03-26 01:41:48 -04:00
90aada4bf3 ADd service offering for motolite users for jumpstart warranty. Add sql file to add new service offering. #795 2024-03-25 04:51:40 -04:00
93c313f111 Fix issues found during testing. #794 2024-03-24 23:38:08 -04:00
Ramon Gutierrez
25e0931f6f Revert warranty serial check for now #783 2024-03-23 04:03:00 +08:00
Ramon Gutierrez
5af3a3cb5e Add check for if serial is present before registering warranty #783 2024-03-23 04:01:16 +08:00
bf0d1f664b Add CAPI calls for hub list and job order details. #794 2024-03-22 05:35:06 -04:00
Ramon Gutierrez
07b459e7a3 Add pycache and test insurance banner to gitignore #783 2024-03-19 19:16:34 +08:00
Ramon Gutierrez
c4e03f861d Exclude cancelled JOs on rider app API #783 2024-03-19 17:33:13 +08:00
Ramon Gutierrez
cd21e41d2f Merge branch '793-prevent-jo-progression-on-rider-app-after-cancellation' into '783-rider-app-trade-in-support'
Rename JO progression check method #793

See merge request jankstudio/resq!905
2024-03-19 08:36:23 +00:00
Ramon Gutierrez
1a6af00399 Rename JO progression check method #793 2024-03-19 16:35:51 +08:00
Ramon Gutierrez
c03025748f Merge branch '793-prevent-jo-progression-on-rider-app-after-cancellation' into '783-rider-app-trade-in-support'
Resolve "Prevent JO progression on rider app after cancellation"

See merge request jankstudio/resq!904
2024-03-19 08:33:43 +00:00
Ramon Gutierrez
ca513355fe Switch parameters of allowJOProgress to fix error #793 2024-03-19 15:25:32 +08:00
Ramon Gutierrez
9152370300 Add checks to prevent JO progression if cancelled #793 2024-03-19 15:22:34 +08:00
Ramon Gutierrez
bff89a6817 Add regional pricing support to job order updates on rider app #783 2024-03-15 16:24:09 +08:00
Ramon Gutierrez
a59aa0f66d Make insurance body types a namevalue class #783 2024-03-14 13:40:37 +08:00
Ramon Gutierrez
69218aecf4 Update insurance application paid call to use external transaction id #783 2024-03-13 14:33:29 +08:00
Ramon Gutierrez
47dcd92474 Refactor insurance, paymongo connector logging logic #783 2024-03-13 07:09:56 +08:00
Ramon Gutierrez
9f4c16b149 Add debug code for insurance api #783 2024-03-12 16:34:21 +08:00
Ramon Gutierrez
175ac92765 Fix syntax error on rider api controller #783 2024-03-11 23:50:44 +08:00
Ramon Gutierrez
801a274e8c Disable serial number requirement on rider api to allow old rider app to work for now #783 2024-03-11 23:45:05 +08:00
Ramon Gutierrez
d603934d93 Update the correct MQTT client with latest changes #783 2024-03-07 23:41:21 +08:00
Ramon Gutierrez
a4b883b7ea Fix rider assign MQTT event for new rider app #783 2024-03-07 23:39:57 +08:00
Ramon Gutierrez
a2cd86b48c Merge branch '780-regional-pricing' into 783-rider-app-trade-in-support
# Conflicts:
#	src/Controller/CAPI/RiderAppController.php
#	src/InvoiceRule/BatterySales.php
2024-03-07 09:42:42 +08:00
5ddebcd95f Add saving of facilitated hub data when reassigning hub. Fix display of dropdown for facilitated hubs. #792 2024-03-04 22:45:57 -05:00
Ramon Gutierrez
ef5e629358 Merge branch '791-support-for-insurance-premiums-banner' into 780-regional-pricing 2024-02-23 18:40:10 +08:00
Ramon Gutierrez
4dd8efd95a Fix returned format of body types endpoint #791 2024-02-23 18:39:21 +08:00
34adefb798 Merge branch '791-support-for-insurance-premiums-banner' of gitlab.com:jankstudio/resq into 780-regional-pricing 2024-02-23 04:01:58 -05:00
Ramon Gutierrez
78a43ae85c Add insurance body types endpoint #791 2024-02-23 16:53:04 +08:00
7ed9f90945 Merge branch '791-support-for-insurance-premiums-banner' of gitlab.com:jankstudio/resq into 780-regional-pricing 2024-02-22 19:42:15 -05:00
Ramon Gutierrez
0a4f78559c Merge branch '791-support-for-insurance-premiums-banner' into 783-rider-app-trade-in-support 2024-02-22 16:51:55 +08:00
Ramon Gutierrez
270a4cfb10 Add endpoint for insurance premiums banner #791 2024-02-22 16:51:26 +08:00
Ramon Gutierrez
7a5583d840 Include trade in type and container size to battery info rider api call #783 2024-02-22 16:08:36 +08:00
4d89e7420f Add checking for location and price tier when getting list of compatible batteries. #780 2024-02-20 02:09:57 -05:00
010bdca458 Add updating of other fields when updating job order. Fix issue found during testing in invoice rule. #783 2024-02-12 06:19:00 -05:00
abf4bbfe22 Add checking for promo. #783 2024-02-12 03:46:26 -05:00
Ramon Gutierrez
eebd1d93c4 Fix battery info endpoint to work with warranty serials instead #783 2024-02-08 23:17:32 +08:00
Ramon Gutierrez
ba09e6ac7b Merge branch '782-invoice-and-jo-modifications-for-regional-pricing' into '780-regional-pricing'
Resolve "Invoice and JO modifications for regional pricing"

See merge request jankstudio/resq!894
2024-02-08 06:21:40 +00:00
Ramon Gutierrez
8860796db2 Add endpoint for rider app trade-in types #783 2024-02-07 15:02:47 +08:00
Ramon Gutierrez
3254527653 Merge branch 'master' into '782-invoice-and-jo-modifications-for-regional-pricing'
Master

See merge request jankstudio/resq!901
2024-02-06 08:16:28 +00:00
8ca7292a25 Add processing of trade in items from rider app. #783 2024-02-05 03:06:06 -05:00
c9057b9617 Add API call to update invoice. #783 2024-02-02 04:50:03 -05:00
86744afde3 Add serial as a parameter when rider fulfills a job order. #783 2024-02-01 04:24:09 -05:00
ae46d64f5b Add API call to return battery data given a serial. #783 2024-02-01 03:10:16 -05:00
Ramon Gutierrez
a5340fbfd6 Merge branch '788-remove-geofence-message-from-view-jo' into 'master'
Resolve "Remove geofence message from View JO"

Closes #788

See merge request jankstudio/resq!900
2024-02-01 06:46:29 +00:00
0e5365a015 Comment out geofence message when viewing JO. Initialize branch_codes array. #788 2024-01-31 20:13:41 -05:00
Ramon Gutierrez
313dc74eb0 Merge branch '784-transaction-origin-additional-fields' into 'master'
Fix transaction origin list to replace existing facebook and hotline with manila entries #784

Closes #784

See merge request jankstudio/resq!899
2024-01-31 14:06:05 +00:00
Ramon Gutierrez
8ee1809a99 Merge branch '787-update-geofence-message-in-app' into 'master'
Resolve "Update geofence message in app"

Closes #787

See merge request jankstudio/resq!898
2024-01-30 07:25:15 +00:00
5717eddaa0 Update geofence message in the app. #787 2024-01-30 02:24:08 -05:00
Ramon Gutierrez
35b3b23e55 Merge branch '786-remove-geofence-message-in-crm' into 'master'
Resolve "Remove geofence message in CRM"

Closes #786

See merge request jankstudio/resq!897
2024-01-30 07:14:34 +00:00
f7c8034538 Comment out the geofence call for admin panel. #786 2024-01-30 02:12:08 -05:00
Ramon Gutierrez
ba8ac73c9d Merge branch '785-no-sms-notification-for-discount' into 'master'
Resolve "No SMS notification for discount"

Closes #785

See merge request jankstudio/resq!896
2024-01-30 07:03:47 +00:00
9b52d4578f Add checking for discount rejection reason. #785 2024-01-30 02:02:24 -05:00
Ramon Gutierrez
b033e5674d Merge branch '784-transaction-origin-additional-fields' into 'master'
Resolve "Transaction Origin additional fields"

Closes #784

See merge request jankstudio/resq!895
2024-01-30 06:33:05 +00:00
Ramon Gutierrez
b123be25cc Add battery sizes endpoint to rider api #783 2024-01-30 03:42:05 +08:00
d4eae00902 Add sql for item types. #782 2024-01-28 22:21:34 -05:00
20f5bb08e0 Add checking for longitude and latitude when calling getEstimate. #782 2024-01-26 04:50:54 -05:00
213171f4b7 Add price tiering for Resq 2 App and TAPI. #782 2024-01-26 04:44:18 -05:00
c136b0666b Fix issues found during testing. #782 2024-01-25 03:08:00 -05:00
57fd7fe5ac Fix issues found during testing. #782 2024-01-25 02:23:47 -05:00
bc6364ace5 Add price tier to invoice rules. #782 2024-01-25 02:11:05 -05:00
c5b395d720 Add price tier for battery replacement warranty. #782 2024-01-24 04:02:14 -05:00
b6763bfd3e Add price tier checking for battery sales. #782 2024-01-24 02:24:13 -05:00
29ad8d57a4 Add processing of battery entries and invoice item titles. #782 2024-01-22 04:24:41 -05:00
root
70ee7fdd89 Add invoice rule for price tier. #782 2024-01-19 04:14:06 -05:00
Korina Cordero
ee033ddd55 Fix issues found during testing. #780 2024-01-17 15:31:26 +08:00
Korina Cordero
a83fecf224 Merge branch 'master' of gitlab.com:jankstudio/resq into 780-regional-pricing 2024-01-17 14:50:50 +08:00
Korina Cordero
9de6fa7999 Fix issues found during saving of item prices. #780 2024-01-17 14:25:28 +08:00
Korina Cordero
6d7c8c5b53 Add saving of item prices for price tier. #780 2024-01-16 16:28:26 +08:00
Korina Cordero
022336ad8f Add saving of prices. #780 2024-01-15 17:21:31 +08:00
Korina Cordero
bfe7a5fbf6 Fix display of item prices per tier. #780 2024-01-15 15:23:07 +08:00
Korina Cordero
b592390554 Add response when displaying item prices. #780 2024-01-11 16:48:23 +08:00
Korina Cordero
7f4675a8a2 Add item type dropdown to form. #780 2024-01-10 17:23:15 +08:00
Korina Cordero
f65ca19010 Load service offering into Item Pricing page. #780 2024-01-09 17:39:29 +08:00
c17be92f0a Fix errors for item query. #780 2023-12-31 00:46:12 -05:00
80b9f90324 Rename ItemPriceController to ItemController. #780 2023-12-30 23:09:27 -05:00
01e4baa8c4 Add item price controller. #780 2023-12-28 20:41:44 -05:00
e4ffcc0c9d Add controller for item pricing. #780 2023-12-27 22:15:44 -05:00
root
58f46fd5bf Add route and CRUD for item type. #780 2023-12-21 23:23:46 -05:00
Korina Cordero
8c810bf27a Add validation and deletion for price tier. #780 2023-12-21 16:24:37 +08:00
Korina Cordero
fa3cf12be1 Add controller for price tier. #780 2023-12-20 18:15:17 +08:00
Korina Cordero
9447f64312 Create item price, price tier, and item type entities for regional pricing. #780 2023-12-19 17:30:38 +08:00
95 changed files with 6002 additions and 631 deletions

3
.gitignore vendored
View file

@ -12,3 +12,6 @@
*.swp
/public/warranty_uploads/*
.vscode
*__pycache__
/public/assets/images/insurance-premiums.png

View file

@ -634,6 +634,45 @@ catalyst_auth:
- id: service_offering.delete
label: Delete
- id: price_tier
label: Price Tier
acls:
- id: price_tier.menu
label: Menu
- id: price_tier.list
label: List
- id: price_tier.add
label: Add
- id: price_tier.update
label: Update
- id: price_tier.delete
label: Delete
- id: item_type
label: Item Type
acls:
- id: item_type.menu
label: Menu
- id: item_type.list
label: List
- id: item_type.add
label: Add
- id: item_type.update
label: Update
- id: item_type.delete
label: Delete
- id: item
label: Item
acls:
- id: item.menu
label: Menu
- id: item_pricing
label: Item Pricing
acls:
- id: item_pricing.update
label: Update
api:
user_entity: "App\\Entity\\ApiUser"
acl_data:
@ -712,6 +751,16 @@ catalyst_auth:
acls:
- id: warrantyserial.upload
label: Upload
- id: hub
label: Hub Access
acls:
- id: hub.list
label: List
- id: joborder
label: Job Order Access
acls:
- id: joborder.find
label: Find Job Order
- id: tapi_vmanufacturer
label: Third Party Vehicle Manufacturer Access

View file

@ -177,7 +177,7 @@ catalyst_menu:
acl: support.menu
label: '[menu.support]'
icon: flaticon-support
order: 10
order: 11
- id: customer_list
acl: customer.list
label: '[menu.support.customers]'
@ -223,7 +223,7 @@ catalyst_menu:
acl: service.menu
label: '[menu.service]'
icon: flaticon-squares
order: 11
order: 12
- id: service_list
acl: service.list
label: '[menu.service.services]'
@ -233,7 +233,7 @@ catalyst_menu:
acl: partner.menu
label: '[menu.partner]'
icon: flaticon-network
order: 12
order: 13
- id: partner_list
acl: partner.list
label: '[menu.partner.partners]'
@ -247,7 +247,7 @@ catalyst_menu:
acl: motolite_event.menu
label: '[menu.motolite_event]'
icon: flaticon-event-calendar-symbol
order: 13
order: 14
- id: motolite_event_list
acl: motolite_event.list
label: '[menu.motolite_event.events]'
@ -257,7 +257,7 @@ catalyst_menu:
acl: analytics.menu
label: '[menu.analytics]'
icon: flaticon-graphic
order: 14
order: 15
- id: analytics_forecast_form
acl: analytics.forecast
label: '[menu.analytics.forecasting]'
@ -267,7 +267,7 @@ catalyst_menu:
acl: database.menu
label: '[menu.database]'
icon: fa fa-database
order: 15
order: 16
- id: ticket_type_list
acl: ticket_type.menu
label: '[menu.database.tickettypes]'
@ -288,3 +288,21 @@ catalyst_menu:
acl: service_offering.menu
label: '[menu.database.serviceofferings]'
parent: database
- id: item_type_list
acl: item_type.menu
label: '[menu.database.itemtypes]'
parent: database
- id: item
acl: item.menu
label: Item Management
icon: fa fa-boxes
order: 10
- id: price_tier_list
acl: price_tier.list
label: Price Tiers
parent: item
- id: item_pricing
acl: item_pricing.update
label: Item Pricing
parent: item

View file

@ -303,3 +303,13 @@ apiv2_insurance_application_create:
path: /apiv2/insurance/application
controller: App\Controller\CustomerAppAPI\InsuranceController::createApplication
methods: [POST]
apiv2_insurance_premiums_banner:
path: /apiv2/insurance/premiums_banner
controller: App\Controller\CustomerAppAPI\InsuranceController::getPremiumsBanner
methods: [GET]
apiv2_insurance_body_types:
path: /apiv2/insurance/body_types
controller: App\Controller\CustomerAppAPI\InsuranceController::getBodyTypes
methods: [GET]

View file

@ -194,3 +194,16 @@ capi_warranty_serial_upload:
path: /capi/warranty_serial/upload
controller: App\Controller\CAPI\WarrantySerialController::uploadWarrantySerialFile
methods: [POST]
# pullout form system
# hub
capi_hub_list:
path: /capi/hubs
controller: App\Controller\CAPI\HubController::getAll
methods: [GET]
# job order details
capi_job_order:
path: /capi/job_order/{id}
controller: App\Controller\CAPI\JobOrderController::getJobOrder
methods: [GET]

View file

@ -94,3 +94,24 @@ capi_rider_jo_start:
path: /rider_api/start
controller: App\Controller\CAPI\RiderAppController::startJobOrder
methods: [POST]
# trade-ins
capi_rider_battery_sizes:
path: /rider_api/battery_sizes
controller: App\Controller\CAPI\RiderAppController::getBatterySizes
methods: [GET]
capi_rider_trade_in_types:
path: /rider_api/trade_in_types
controller: App\Controller\CAPI\RiderAppController::getTradeInTypes
methods: [GET]
capi_rider_battery_info:
path: /rider_api/battery/{serial}
controller: App\Controller\CAPI\RiderAppController::getBatteryInfo
methods: [GET]
capi_rider_update_jo:
path: /rider_api/job_order/update
controller: App\Controller\CAPI\RiderAppController::updateJobOrder
methods: [POST]

View file

@ -0,0 +1,14 @@
item_pricing:
path: /item-pricing
controller: App\Controller\ItemPricingController::index
methods: [GET]
item_pricing_update:
path: /item-pricing
controller: App\Controller\ItemPricingController::formSubmit
methods: [POST]
item_pricing_prices:
path: /item-pricing/{pt_id}/{it_id}/prices
controller: App\Controller\ItemPricingController::itemPrices
methods: [GET]

View file

@ -0,0 +1,34 @@
item_type_list:
path: /item-types
controller: App\Controller\ItemTypeController::index
methods: [GET]
item_type_rows:
path: /item-types/rowdata
controller: App\Controller\ItemTypeController::datatableRows
methods: [POST]
item_type_add_form:
path: /item-types/newform
controller: App\Controller\ItemTypeController::addForm
methods: [GET]
item_type_add_submit:
path: /item-types
controller: App\Controller\ItemTypeController::addSubmit
methods: [POST]
item_type_update_form:
path: /item-types/{id}
controller: App\Controller\ItemTypeController::updateForm
methods: [GET]
item_type_update_submit:
path: /item-types/{id}
controller: App\Controller\ItemTypeController::updateSubmit
methods: [POST]
item_type_delete:
path: /item-types/{id}
controller: App\Controller\ItemTypeController::deleteSubmit
methods: [DELETE]

View file

@ -0,0 +1,34 @@
price_tier_list:
path: /price-tiers
controller: App\Controller\PriceTierController::index
methods: [GET]
price_tier_rows:
path: /price-tiers/rows
controller: App\Controller\PriceTierController::datatableRows
methods: [POST]
price_tier_add_form:
path: /price-tiers/newform
controller: App\Controller\PriceTierController::addForm
methods: [GET]
price_tier_add_submit:
path: /price-tiers
controller: App\Controller\PriceTierController::addSubmit
methods: [POST]
price_tier_update_form:
path: /price-tiers/{id}
controller: App\Controller\PriceTierController::updateForm
methods: [GET]
price_tier_update_submit:
path: /price-tiers/{id}
controller: App\Controller\PriceTierController::updateSubmit
methods: [POST]
price_tier_delete:
path: /price-tiers/{id}
controller: App\Controller\PriceTierController::deleteSubmit
methods: [DELETE]

View file

@ -51,7 +51,7 @@ tapi_vehicle_make_list:
tapi_battery_list:
path: /tapi/vehicles/{vid}/compatible_batteries
controller: App\Controller\TAPI\BatteryController::getCompatibleBatteries
methods: [GET]
methods: [POST]
# promos
tapi_promo_list:

View file

@ -15,6 +15,8 @@ parameters:
api_version: "%env(API_VERSION)%"
android_app_version: "%env(ANDROID_APP_VERSION)%"
ios_app_version: "%env(IOS_APP_VERSION)%"
insurance_premiums_banner_url: "%env(INSURANCE_PREMIUMS_BANNER_URL)%"
enabled_hub_filters: "%env(ENABLED_HUB_FILTERS)%"
services:
# default configuration for services in *this* file
@ -108,6 +110,12 @@ services:
arguments:
$callback_url: "%env(WARRANTY_SERIAL_CALLBACK_URL)%"
App\Command\ProcessLatePaymongoTransactionsCommand:
arguments:
$em: "@doctrine.orm.entity_manager"
$paymongo: "@App\\Service\\PayMongoConnector"
$webhook_id: "%env(PAYMONGO_WEBHOOK_ID)%"
# rider tracker service
App\Service\RiderTracker:
arguments:
@ -310,3 +318,41 @@ services:
arguments:
$server_key: "%env(FCM_SERVER_KEY)%"
$sender_id: "%env(FCM_SENDER_ID)%"
# price tier manager
App\Service\PriceTierManager:
arguments:
$em: "@doctrine.orm.entity_manager"
# hub filters
App\Service\HubFilter\BaseHubFilter:
arguments:
$hub_filter_logger: "@App\\Service\\HubFilterLogger"
$em: "@doctrine.orm.entity_manager"
$rt: "@App\\Service\\RisingTideGateway"
$trans: "@Symfony\\Contracts\\Translation\\TranslatorInterface"
App\Service\HubFilter\Filters\DateAndTimeHubFilter:
public: true
App\Service\HubFilter\Filters\JoTypeHubFilter:
public: true
App\Service\HubFilter\Filters\MaxResultsHubFilter:
public: true
App\Service\HubFilter\Filters\PaymentMethodHubFilter:
public: true
App\Service\HubFilter\Filters\RiderAvailabilityHubFilter:
public: true
App\Service\HubFilter\Filters\InventoryHubFilter:
public: true
arguments:
$im: "@App\\Service\\InventoryManager"
App\Service\HubFilter\Filters\RoundRobinHubFilter:
public: true
arguments:
$hub_distributor: "@App\\Service\\HubDistributor"

View file

@ -0,0 +1,136 @@
<?php
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
use Doctrine\ORM\EntityManagerInterface;
use App\Ramcar\TransactionStatus;
use App\Entity\GatewayTransaction;
use App\Service\PayMongoConnector;
use DateTime;
class ProcessLatePaymongoTransactionsCommand extends Command
{
protected $em;
protected $paymongo;
protected $webhook_id;
public function __construct(EntityManagerInterface $em, PayMongoConnector $paymongo, $webhook_id)
{
$this->em = $em;
$this->paymongo = $paymongo;
$this->webhook_id = $webhook_id;
parent::__construct();
}
protected function configure()
{
$this->setName('paymongo:checkpending')
->setDescription('Check for any late PayMongo transactions and process if needed.')
->setHelp('Check for any late PayMongo transactions and process if needed.')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Ignore webhook status and process anyway.');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$force = $input->getOption('force');
// if we aren't forcing, check webhook status first
if (!$force) {
$output->writeln('Checking webhook status...');
// check if webhook is disabled
$webhook = $this->paymongo->getWebhook($this->webhook_id);
if ($webhook['success'] && $webhook['response']['data']['attributes']['status'] === 'enabled') {
$output->writeln('<info>Webhook is enabled, no need to do anything.</info>');
return 0;
} else {
$output->writeln('<comment>Webhook is disabled! Logging event and attempting to re-enable...</comment>');
// attempt re-enabling of webhook
$result = $this->paymongo->enableWebhook($this->webhook_id);
if ($result['success'] && $result['response']['data']['attributes']['status'] ?? null === 'enabled') {
$output->writeln('<info>Webhook ' . $this->webhook_id . ' re-enabled!</info>');
// log event
$this->paymongo->log('WEBHOOK RE-ENABLED', "[]", json_encode($result['response'], JSON_PRETTY_PRINT), 'webhook');
} else {
$output->writeln('<comment>Webhook ' . $this->webhook_id . ' could not be re-enabled.</comment>');
// log event
$this->paymongo->log('WEBHOOK FAILURE', "[]", json_encode($result['response'], JSON_PRETTY_PRINT), 'webhook');
}
}
}
$output->writeln('Fetching all late pending transactions...');
// set date threshold to 24 hours ago
$date_threshold = (new DateTime())->modify('-24 hours');
$transactions = $this->em->getRepository(GatewayTransaction::class)
->createQueryBuilder('t')
->select('t')
->where('t.status = :status')
->andWhere('t.date_create <= :date_threshold')
->setParameter('status', TransactionStatus::PENDING)
->setParameter('date_threshold', $date_threshold)
->getQuery()
->getResult();
$output->writeln('Found '. count($transactions) . ' rows matching criteria.');
$x = 0;
foreach ($transactions as $trans) {
// check paymongo status
$checkout = $this->paymongo->getCheckout($trans->getExtTransactionId());
if ($checkout['success']) {
// check if we have any payments made
$payments = $checkout['response']['data']['attributes']['payments'] ?? [];
if (!empty($payments)) {
$amount_paid = 0;
// for good measure, we get all successful payments and add them up
foreach ($payments as $payment) {
if ($payment['attributes']['status'] === TransactionStatus::PAID) {
$amount_paid = bcadd($amount_paid, $payment['attributes']['amount']);
}
}
// this transaction is fully paid, so we mark it as paid
if (bccomp($trans->getAmount(), $amount_paid) <= 0) {
$trans->setStatus(TransactionStatus::PAID);
$trans->setDatePay(new DateTime());
$this->em->flush();
$output->writeln('Marked transaction '. $trans->getID() . ' as paid.');
$x++;
} else {
$output->writeln('<comment>Insufficient payment amount (' . $amount_paid . '/' . $trans->getAmount() . ') for this transaction: ' . $trans->getID() . '</comment>');
}
} else {
$output->writeln('<comment>No payments found for transaction: ' . $trans->getID() . '</comment>');
}
} else {
$output->writeln('<comment>Checkout not found: ' . $checkout['error']['message'] . '</comment>');
}
}
$output->writeln('<info>Done! Processed ' . $x . ' rows.</info>');
return 0;
}
}

View file

@ -50,6 +50,7 @@ use App\Service\HubFilterLogger;
use App\Service\HubFilteringGeoChecker;
use App\Service\HashGenerator;
use App\Service\JobOrderManager;
use App\Service\PriceTierManager;
use App\Entity\MobileSession;
use App\Entity\Customer;
@ -2911,6 +2912,10 @@ class APIController extends Controller implements LoggedController
// old app doesn't have separate jumpstart
$icrit->setSource(TransactionOrigin::CALL);
// set price tier
$pt_id = $this->pt_manager->getPriceTier($jo->getCoordinates());
$icrit->setPriceTier($pt_id);
// check promo
$promo_id = $req->request->get('promo_id');
if (!empty($promo_id))

View file

@ -326,7 +326,7 @@ class APIRoleController extends Controller
protected function padAPIACLHierarchy(&$params)
{
// get acl keys hierarchy
$api_acl_data = $this->api_acl_gen->getACL();
$api_acl_data = $this->api_acl_gen->getACL('api');
$params['api_acl_hierarchy'] = $api_acl_data['hierarchy'];
}

View file

@ -0,0 +1,53 @@
<?php
namespace App\Controller\CAPI;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\Query;
use Doctrine\ORM\EntityManagerInterface;
use Catalyst\ApiBundle\Controller\ApiController;
use Catalyst\ApiBundle\Component\Response as APIResponse;
use App\Entity\Hub;
use Catalyst\AuthBundle\Service\ACLGenerator as ACLGenerator;
class HubController extends ApiController
{
protected $acl_gen;
public function __construct(ACLGenerator $acl_gen)
{
$this->acl_gen = $acl_gen;
}
public function getAll(EntityManagerInterface $em)
{
// get all hub data order by name
$this->denyAccessUnlessGranted('hub.list', null, 'No access.');
$results = $em->getRepository(Hub::class)->findBy([], ['name' => 'ASC']);
$hubs = [];
foreach($results as $res)
{
$hub_id = $res->getId();
$hub_name = $res->getName();
$hub_address = $res->getAddress();
$hub_branch_code = $res->getBranchCode();
$hubs[$hub_id] = [
'id' => $hub_id,
'name' => $hub_name,
'address' => $hub_address,
'branch_code' => $hub_branch_code,
];
}
$data = [
'hubs' => $hubs,
];
return new APIResponse(true, 'Hubs loaded.', $data);
}
}

View file

@ -0,0 +1,139 @@
<?php
namespace App\Controller\CAPI;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\Query;
use Doctrine\ORM\EntityManagerInterface;
use Catalyst\ApiBundle\Controller\ApiController;
use Catalyst\ApiBundle\Component\Response as APIResponse;
use App\Entity\JobOrder;
use App\Entity\Warranty;
use App\Ramcar\JOStatus;
use Catalyst\AuthBundle\Service\ACLGenerator as ACLGenerator;
class JobOrderController extends ApiController
{
protected $acl_gen;
public function __construct(ACLGenerator $acl_gen)
{
$this->acl_gen = $acl_gen;
}
public function getJobOrder($id, EntityManagerInterface $em)
{
$this->denyAccessUnlessGranted('joborder.find', null, 'No access.');
$jo = $em->getRepository(JobOrder::class)->find($id);
if ($jo == null)
return new APIResponse(false, 'No job order found with that number.', null, 404);
$data = $this->generateJobOrderData($jo, $em);
return new APIResponse(true, 'Job order found.', $data);
}
protected function generateJobOrderData($jo, EntityManagerInterface $em)
{
// customer vehicle
$cv = $jo->getCustomerVehicle();
// customer information
$customer = $jo->getCustomer();
// hub
$hub_name = '';
$hub = $jo->getHub();
if ($hub != null)
$hub_name = $hub->getName();
// check if JO is fulfilled, if not, we leave date_purchase blank
$date_purchase = '';
$serial = '';
$status = $jo->getStatus();
if ($status == JOStatus::FULFILLED)
{
if ($jo->getDateFulfill() != null)
$date_purchase = $jo->getDateFulfill()->format('M d, Y H:i');
// find warranty to get the serial using plate number
$serial = $this->getSerialFromWarranty($cv->getPlateNumber(), $em);
}
$jo_data = [
'id' => $jo->getID(),
'first_name' => $customer->getFirstName(),
'last_name' => $customer->getLastName(),
'mobile_number' => $customer->getPhoneMobile(),
'email' => $customer->getEmail(),
'plate_number' => $cv->getPlateNumber(),
'date_purchase' => $date_purchase,
'address' => $jo->getDeliveryAddress(),
'hub' => $hub_name,
'serial' => $serial,
];
// invoice items
$items = [];
$jo_items = $jo->getInvoice()->getItems();
$non_battery_item_titles = ['Promo discount', 'Trade-in', 'Service'];
foreach ($jo_items as $item)
{
$item_title = $item->getTitle();
// check if title has Promo discount, Trade-in, or Service
$flag_battery = $this->checkIfBatteryInvoiceItem($item_title, $non_battery_item_titles);
if ($flag_battery == true)
{
$items[] = [
'title' => $item->getTitle(),
];
}
}
$jo_data['items'] = $items;
return $jo_data;
}
protected function checkIfBatteryInvoiceItem($item_title, $non_battery_item_titles)
{
foreach ($non_battery_item_titles as $nb_item_title)
{
$pos_result = stripos($item_title, $nb_item_title);
// if found, invoice item is not a battery item
if ($pos_result !== false)
return false;
}
return true;
}
protected function getSerialFromWarranty($plate_number, EntityManagerInterface $em)
{
// NOTE: Modify the search for the latest warranty. This seems hacky.
// get latest warranty using plate number
$warranty_results = $em->getRepository(Warranty::class)->findBy(
['plate_number' => $plate_number],
['date_create' => 'desc']
);
$serial = '';
if (!empty($warranty_results))
{
// get first entry
$warranty = current($warranty_results);
$serial = $warranty->getSerial();
}
return $serial;
}
}

View file

@ -23,7 +23,9 @@ use App\Entity\BatterySize;
use App\Entity\RiderAPISession;
use App\Entity\User;
use App\Entity\ApiUser as APIUser;
use App\Entity\JobOrder;
use App\Entity\SAPBattery;
use App\Entity\WarrantySerial;
use App\Service\RedisClientProvider;
use App\Service\RiderCache;
use App\Service\MQTTClient;
@ -34,6 +36,7 @@ use App\Service\JobOrderHandlerInterface;
use App\Service\InvoiceGeneratorInterface;
use App\Service\RisingTideGateway;
use App\Service\RiderTracker;
use App\Service\PriceTierManager;
use App\Ramcar\ServiceType;
use App\Ramcar\TradeInType;
@ -286,8 +289,9 @@ class RiderAppController extends ApiController
// do we have a job order?
// $jo = $rider->getActiveJobOrder();
// NOTE: we do not include job orders that have been cancelled
$jo = $rider->getCurrentJobOrder();
if ($jo == null)
if ($jo == null || $jo->getStatus() == JOStatus::CANCELLED)
{
$data = [
'job_order' => null
@ -382,6 +386,7 @@ class RiderAppController extends ApiController
'flag_coolant' => $jo->hasCoolant(),
'has_motolite' => $cv->hasMotoliteBattery(),
'delivery_status' => $jo->getDeliveryStatus(),
'flag_sealant' => $jo->hasSealant(),
]
];
}
@ -408,6 +413,11 @@ class RiderAppController extends ApiController
if (!empty($msg))
return new APIResponse(false, $msg);
// check if JO can be modified first
if (!$this->checkJOProgressionAllowed($em, $jo, $rider)) {
return new APIResponse(false, 'Job order can no longer be modified.');
}
// TODO: refactor this into a jo handler class, so we don't have to repeat for control center
// set jo status to in transit
@ -458,6 +468,11 @@ class RiderAppController extends ApiController
// TODO: this is a workaround for requeue, because rider app gets stuck in accept / decline screen
return new APIResponse(true, $msg);
// check if JO can be modified first
if (!$this->checkJOProgressionAllowed($em, $jo, $rider)) {
return new APIResponse(false, 'Job order can no longer be modified.');
}
// requeue it, instead of cancelling it
$jo->requeue();
@ -516,6 +531,11 @@ class RiderAppController extends ApiController
// get rider's current job order
$jo = $rider->getCurrentJobOrder();
// check if JO can be modified first
if (!$this->checkJOProgressionAllowed($em, $jo, $rider)) {
return new APIResponse(false, 'Job order can no longer be modified.');
}
// set delivery status
$jo->setDeliveryStatus(DeliveryStatus::RIDER_DEPART_HUB);
@ -556,6 +576,11 @@ class RiderAppController extends ApiController
// get rider's current job order
$jo = $rider->getCurrentJobOrder();
// check if JO can be modified first
if (!$this->checkJOProgressionAllowed($em, $jo, $rider)) {
return new APIResponse(false, 'Job order can no longer be modified.');
}
// set delivery status
$jo->setDeliveryStatus(DeliveryStatus::RIDER_ARRIVE_HUB_PRE_JO);
@ -596,6 +621,11 @@ class RiderAppController extends ApiController
// get rider's current job order
$jo = $rider->getCurrentJobOrder();
// check if JO can be modified first
if (!$this->checkJOProgressionAllowed($em, $jo, $rider)) {
return new APIResponse(false, 'Job order can no longer be modified.');
}
// set delivery status
$jo->setDeliveryStatus(DeliveryStatus::RIDER_DEPART_HUB_PRE_JO);
@ -636,6 +666,11 @@ class RiderAppController extends ApiController
// get rider's current job order
$jo = $rider->getCurrentJobOrder();
// check if JO can be modified first
if (!$this->checkJOProgressionAllowed($em, $jo, $rider)) {
return new APIResponse(false, 'Job order can no longer be modified.');
}
// set delivery status
$jo->setDeliveryStatus(DeliveryStatus::RIDER_START);
@ -677,6 +712,11 @@ class RiderAppController extends ApiController
// set jo status to in progress
$jo->setStatus(JOStatus::IN_PROGRESS);
// check if JO can be modified first
if (!$this->checkJOProgressionAllowed($em, $jo, $rider)) {
return new APIResponse(false, 'Job order can no longer be modified.');
}
// set delivery status
$jo->setDeliveryStatus(DeliveryStatus::RIDER_ARRIVE);
@ -735,6 +775,11 @@ class RiderAppController extends ApiController
// get rider's current job order
$jo = $rider->getCurrentJobOrder();
// check if JO can be modified first
if (!$this->checkJOProgressionAllowed($em, $jo, $rider)) {
return new APIResponse(false, 'Job order can no longer be modified.');
}
// set delivery status
$jo->setDeliveryStatus(DeliveryStatus::RIDER_ARRIVE_HUB);
@ -758,6 +803,54 @@ class RiderAppController extends ApiController
return new APIResponse(true, 'Rider arrive at hub.', $data);
}
public function getBatterySizes(Request $req, EntityManagerInterface $em)
{
// get capi user
$capi_user = $this->getUser();
if ($capi_user == null)
return new APIResponse(false, 'User not found.');
// get rider id from capi user metadata
$rider = $this->getRiderFromCAPI($capi_user, $em);
if ($rider == null)
return new APIResponse(false, 'No rider found.');
// get sizes
$qb = $em->getRepository(BatterySize::class)
->createQueryBuilder('bs');
$sizes = $qb->select('bs.id, bs.name')
->orderBy('bs.name', 'asc')
->getQuery()
->getResult();
// response
return new APIResponse(true, '', [
'sizes' => $sizes,
]);
}
public function getTradeInTypes(Request $req, EntityManagerInterface $em)
{
// get capi user
$capi_user = $this->getUser();
if ($capi_user == null)
return new APIResponse(false, 'User not found.');
// get rider id from capi user metadata
$rider = $this->getRiderFromCAPI($capi_user, $em);
if ($rider == null)
return new APIResponse(false, 'No rider found.');
// get trade-in types
$types = TradeInType::getCollection();
// response
return new APIResponse(true, '', [
'types' => $types,
]);
}
public function payment(Request $req, EntityManagerInterface $em, JobOrderHandlerInterface $jo_handler,
RisingTideGateway $rt, WarrantyHandler $wh, MQTTClient $mclient, MQTTClientApiv2 $mclientv2, FCMSender $fcmclient, TranslatorInterface $translator)
{
@ -777,6 +870,22 @@ class RiderAppController extends ApiController
if (!empty($msg))
return new APIResponse(false, $msg);
// check if JO can be modified first
if (!$this->checkJOProgressionAllowed($em, $jo, $rider)) {
return new APIResponse(false, 'Job order can no longer be modified.');
}
// need to check if service type is battery sales
// if so, serial is a required parameter
$serial = $req->request->get('serial', '');
if ($jo->getServiceType() == ServiceType::BATTERY_REPLACEMENT_NEW)
{
/*
if (empty($serial))
return new APIResponse(false, 'Missing parameter(s): serial');
*/
}
// set invoice to paid
$jo->getInvoice()->setStatus(InvoiceStatus::PAID);
@ -828,7 +937,6 @@ class RiderAppController extends ApiController
// create warranty
if($jo_handler->checkIfNewBattery($jo))
{
$serial = null;
$warranty_class = $jo->getWarrantyClass();
$first_name = $jo->getCustomer()->getFirstName();
$last_name = $jo->getCustomer()->getLastName();
@ -912,6 +1020,11 @@ class RiderAppController extends ApiController
// get rider's current job order
$jo = $rider->getCurrentJobOrder();
// check if JO can be modified first
if (!$this->checkJOProgressionAllowed($em, $jo, $rider)) {
return new APIResponse(false, 'Job order can no longer be modified.');
}
// set delivery status
$jo->setDeliveryStatus(DeliveryStatus::RIDER_ARRIVE_HUB_POST_JO);
@ -953,6 +1066,11 @@ class RiderAppController extends ApiController
// get rider's current job order
$jo = $rider->getCurrentJobOrder();
// check if JO can be modified first
if (!$this->checkJOProgressionAllowed($em, $jo, $rider)) {
return new APIResponse(false, 'Job order can no longer be modified.');
}
// set delivery status
$jo->setDeliveryStatus(DeliveryStatus::RIDER_DEPART_HUB_POST_JO);
@ -1099,7 +1217,178 @@ class RiderAppController extends ApiController
return new APIResponse(true, 'Batteries found.', $data);
}
public function changeService(Request $req, EntityManagerInterface $em, InvoiceGeneratorInterface $ic)
public function getBatteryInfo(Request $req, $serial, EntityManagerInterface $em)
{
if (empty($serial))
{
return new APIResponse(false, 'Missing parameter(s): serial');
}
// get capi user
$capi_user = $this->getUser();
if ($capi_user == null)
return new APIResponse(false, 'User not found.');
// get rider id from capi user metadata
$rider = $this->getRiderFromCAPI($capi_user, $em);
if ($rider == null)
return new APIResponse(false, 'No rider found.');
// find battery given serial/sap_code and flag_active is true
$serial = $em->getRepository(WarrantySerial::class)->find($serial);
if (empty($serial)) {
return new APIResponse(false, 'Warranty serial number not found.');
}
$sap_battery = $em->getRepository(SAPBattery::class)->find($serial->getSKU());
if (empty($sap_battery)) {
return new APIResponse(false, 'No battery info found.');
}
$battery = [
'id' => $sap_battery->getID(),
'brand' => $sap_battery->getBrand()->getName(),
'size' => $sap_battery->getSize()->getName(),
'size_id' => $sap_battery->getSize()->getID(),
'trade_in_type' => TradeInType::MOTOLITE,
'container_size' => $sap_battery->getContainerSize()->getName(),
];
return new APIResponse(true, 'Battery info found.', [
'battery' => $battery,
]);
}
public function updateJobOrder(Request $req, EntityManagerInterface $em, InvoiceGeneratorInterface $ic, PriceTierManager $pt_manager)
{
$items = json_decode(file_get_contents('php://input'), true);
// get job order id
if (!isset($items['jo_id']))
return new APIResponse(false, 'Missing parameter(s): jo_id');
// validate jo_id
$jo_id = $items['jo_id'];
if (empty($jo_id) || $jo_id == null)
return new APIResponse(false, 'Missing parameter(s): jo_id');
// get capi user
$capi_user = $this->getUser();
if ($capi_user == null)
return new APIResponse(false, 'User not found.');
// get rider id from capi user metadata
$rider = $this->getRiderFromCAPI($capi_user, $em);
if ($rider == null)
return new APIResponse(false, 'No rider found.');
// get the job order
$jo = $em->getRepository(JobOrder::class)->find($jo_id);
// check if JO can be modified first
if (!$this->checkJOProgressionAllowed($em, $jo, $rider)) {
return new APIResponse(false, 'Job order can no longer be modified.');
}
// check if we have trade in items
$ti_items = [];
if (isset($items['trade_in_items']))
{
// validate the trade in items first
$ti_items = $items['trade_in_items'];
$msg = $this->validateTradeInItems($em, $ti_items);
if (!empty($msg))
return new APIResponse(false, $msg);
}
// get the service type
if (!isset($items['stype_id']))
return new APIResponse(false, 'Missing parameter(s): stype_id');
// validate service type
$stype_id = $items['stype_id'];
if (!ServiceType::validate($stype_id))
return new APIResponse(false, 'Invalid service type - ' . $stype_id);
// save service type
$jo->setServiceType($stype_id);
// validate promo if any. Promo not required
$promo = null;
if (isset($items['promo_id']))
{
$promo_id = $items['promo_id'];
$promo = $em->getRepository(Promo::class)->find($promo_id);
if ($promo == null)
return new APIResponse(false, 'Invalid promo id - ' . $promo_id);
}
// get other parameters, if any: has motolite battery, has warranty doc, with coolant, payment method, with sealant
if (isset($items['flag_motolite_battery']))
{
// get customer vehicle from jo
$cv = $jo->getCustomerVehicle();
$has_motolite = $items['flag_motolite_battery'];
if ($has_motolite == 'true')
$cv->setHasMotoliteBattery(true);
else
$cv->setHasMotoliteBattery(false);
$em->persist($cv);
}
if (isset($items['flag_warranty_doc']))
{
// TODO: what do we do?
}
if (isset($items['flag_coolant']))
{
$has_coolant = $items['flag_coolant'];
if ($has_coolant == 'true')
$jo->setHasCoolant(true);
else
$jo->setHasCoolant(false);
}
if (isset($items['mode_of_payment']))
{
$payment_method = $items['payment_method'];
if (!ModeOfPayment::validate($payment_method))
$payment_method = ModeOfPayment::CASH;
$jo->setModeOfPayment($payment_method);
}
if (isset($items['flag_sealant']))
{
$has_sealant = $items['flag_sealant'];
if ($has_sealant == 'true')
$jo->setHasSealant(true);
else
$jo->setHasSealant(false);
}
// get capi user
$capi_user = $this->getUser();
if ($capi_user == null)
return new APIResponse(false, 'User not found.');
// get rider id from capi user metadata
$rider = $this->getRiderFromCAPI($capi_user, $em);
if ($rider == null)
return new APIResponse(false, 'No rider found.');
// need to get the existing invoice items using jo id and invoice id
$existing_ii = $this->getInvoiceItems($em, $jo);
$this->generateUpdatedInvoice($em, $ic, $jo, $existing_ii, $ti_items, $promo, $pt_manager);
$data = [];
return new APIResponse(true, 'Job order updated.', $data);
}
public function changeService(Request $req, EntityManagerInterface $em, InvoiceGeneratorInterface $ic, PriceTierManager $pt_manager)
{
// $this->debugRequest($req);
@ -1120,6 +1409,11 @@ class RiderAppController extends ApiController
if (!empty($msg))
return new APIResponse(false, $msg);
// check if JO can be modified first
if (!$this->checkJOProgressionAllowed($em, $jo, $rider)) {
return new APIResponse(false, 'Job order can no longer be modified.');
}
// check service type
$stype_id = $req->request->get('stype_id');
if (!ServiceType::validate($stype_id))
@ -1159,6 +1453,13 @@ class RiderAppController extends ApiController
else
$jo->setHasCoolant(false);
// sealant
$flag_sealant = $req->request->get('flag_sealant', 'false');
if ($flag_sealant == 'true')
$jo->setHasSealant(true);
else
$jo->setHasSealant(false);
// has motolite battery
$cv = $jo->getCustomerVehicle();
$has_motolite = $req->request->get('has_motolite', 'false');
@ -1203,6 +1504,10 @@ class RiderAppController extends ApiController
$crit->setHasCoolant($jo->hasCoolant());
$crit->setIsTaxable();
// set price tier
$pt_id = $pt_manager->getPriceTier($jo->getCoordinates());
$crit->setPriceTier($pt_id);
if ($promo != null)
$crit->addPromo($promo);
@ -1241,6 +1546,168 @@ class RiderAppController extends ApiController
return new APIResponse(true, 'Job order service changed.', $data);
}
protected function generateUpdatedInvoice(EntityManagerInterface $em, InvoiceGeneratorInterface $ic, JobOrder $jo, $existing_ii, $trade_in_items, $promo, PriceTierManager $pt_manager)
{
// get the service type
$stype = $jo->getServiceType();
// get the source
$source = $jo->getSource();
// get the customer vehicle
$cv = $jo->getCustomerVehicle();
// get coolant if any
$flag_coolant = $jo->hasCoolant();
// get sealant if any
$flag_sealant = $jo->hasSealant();
// check if new promo is null
if ($promo == null)
{
// promo not updated from app so check existing invoice
// get the promo id from existing invoice item
$promo_id = $existing_ii['promo_id'];
if ($promo_id == null)
$promo = null;
else
$promo = $em->getRepository(Promo::class)->find($promo_id);
}
// populate Invoice Criteria
$icrit = new InvoiceCriteria();
$icrit->setServiceType($stype)
->setCustomerVehicle($cv)
->setSource($source)
->setHasCoolant($flag_coolant)
->setHasSealant($flag_sealant)
->setIsTaxable();
// set price tier
$pt_id = $pt_manager->getPriceTier($jo->getCoordinates());
$icrit->setPriceTier($pt_id);
// at this point, all information should be valid
// assuming JO information is already valid since this
// is in the system already
// add promo if any to criteria
if ($promo != null)
$icrit->addPromo($promo);
// get the battery purchased from existing invoice items
// add the batteries ordered to criteria
$ii_items = $existing_ii['invoice_items'];
foreach ($ii_items as $ii_item)
{
$batt_id = $ii_item['batt_id'];
$qty = $ii_item['qty'];
$battery = $em->getRepository(Battery::class)->find($batt_id);
$icrit->addEntry($battery, null, $qty);
}
// add the trade in items to the criteria
foreach ($trade_in_items as $ti_item)
{
$batt_size_id = $ti_item['battery_size_id'];
$qty = $ti_item['qty'];
$trade_in_type = $ti_item['trade_in_type'];
$batt_size = $em->getRepository(BatterySize::class)->find($batt_size_id);
$icrit->addTradeInEntry($batt_size, $trade_in_type, $qty);
}
// call generateInvoice
$invoice = $ic->generateInvoice($icrit);
// remove previous invoice
$old_invoice = $jo->getInvoice();
$em->remove($old_invoice);
$em->flush();
// save new invoice
$jo->setInvoice($invoice);
$em->persist($invoice);
// log event?
$event = new JOEvent();
$event->setDateHappen(new DateTime())
->setTypeID(JOEventType::RIDER_EDIT)
->setJobOrder($jo)
->setRider($jo->getRider());
$em->persist($event);
$em->flush();
}
protected function getInvoiceItems(EntityManagerInterface $em, JobOrder $jo)
{
$jo_id = $jo->getID();
$conn = $em->getConnection();
// need to get the ordered battery id and quantity from invoice item
// and the promo from invoice
$query_sql = 'SELECT ii.battery_id AS battery_id, ii.qty AS qty, i.promo_id AS promo_id
FROM invoice_item ii, invoice i
WHERE ii.invoice_id = i.id
AND i.job_order_id = :jo_id
AND ii.battery_id IS NOT NULL';
$query_stmt = $conn->prepare($query_sql);
$query_stmt->bindValue('jo_id', $jo_id);
$results = $query_stmt->executeQuery();
$promo_id = null;
$invoice_items = [];
while ($row = $results->fetchAssociative())
{
$promo_id = $row['promo_id'];
$invoice_items[] = [
'batt_id' => $row['battery_id'],
'qty' => $row['qty'],
'trade_in' => ''
];
}
$data = [
'promo_id' => $promo_id,
'invoice_items' => $invoice_items
];
return $data;
}
protected function validateTradeInItems(EntityManagerInterface $em, $ti_items)
{
$msg = '';
foreach ($ti_items as $ti_item)
{
$bs_id = $ti_item['battery_size_id'];
$ti_type = $ti_item['trade_in_type'];
// validate the battery size id
$batt_size = $em->getRepository(BatterySize::class)->find($bs_id);
if ($batt_size == null)
{
$msg = 'Invalid battery size for trade in: ' . $bs_id;
return $msg;
}
// validate the trade in type
if (!TradeInType::validate($ti_type))
{
$msg = 'Invalid trade in type: ' . $ti_type;
return $msg;
}
}
return $msg;
}
protected function getCAPIUser($id, EntityManagerInterface $em)
{
$capi_user = $em->getRepository(APIUser::class)->find($id);
@ -1320,6 +1787,42 @@ class RiderAppController extends ApiController
return $msg;
}
protected function checkJOProgressionAllowed(EntityManagerInterface $em, JobOrder $jo, &$rider)
{
$allowed = true;
error_log("JO delivery status is " . $jo->getDeliveryStatus() . " (not allowed: " . DeliveryStatus::CANCELLED . ")");
error_log("JO status is " . $jo->getStatus() . " (not allowed: " . JOStatus::CANCELLED . ")");
// TODO: add more statuses to block if needed, hence. this is a failsafe in case MQTT is not working.
// check delivery status
switch ($jo->getDeliveryStatus())
{
case DeliveryStatus::CANCELLED:
$allowed = false;
break;
}
// check JO status as well
switch ($jo->getStatus())
{
case JOStatus::CANCELLED:
$allowed = false;
break;
}
// if this is the rider's current JO, set to null
if (!$allowed) {
if ($rider->getCurrentJobOrder() === $jo) {
$rider->setCurrentJobOrder();
$em->persist($rider);
$em->flush();
}
}
return $allowed;
}
protected function debugRequest(Request $req)
{
$all = $req->request->all();

View file

@ -162,6 +162,6 @@ class ApiController extends BaseApiController
protected function getGeoErrorMessage()
{
return 'Oops! Our service is limited to some areas in Metro Manila, Laguna, Cavite, Pampanga and Baguio only. We will update you as soon as we are able to cover your area';
return 'Our services are currently limited to some areas in Metro Manila, Baguio, Batangas, Laguna, Cavite, Pampanga, and Palawan. We will update you as soon as we are available in your area. Thank you for understanding. Keep safe!';
}
}

View file

@ -18,6 +18,7 @@ use App\Ramcar\InsuranceApplicationStatus;
use App\Ramcar\InsuranceMVType;
use App\Ramcar\InsuranceClientType;
use App\Ramcar\TransactionStatus;
use App\Ramcar\InsuranceBodyType;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use DateTime;
@ -293,6 +294,45 @@ class InsuranceController extends ApiController
]);
}
public function getPremiumsBanner(Request $req)
{
// validate params
$validity = $this->validateRequest($req);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
return new ApiResponse(true, '', [
'url' => $this->getParameter('insurance_premiums_banner_url'),
]);
}
public function getBodyTypes(Request $req)
{
// validate params
$validity = $this->validateRequest($req);
if (!$validity['is_valid']) {
return new ApiResponse(false, $validity['error']);
}
$bt_collection = InsuranceBodyType::getCollection();
$body_types = [];
// NOTE: formatting it this way to match how insurance third party API returns their own stuff, so it's all handled one way on the app
foreach ($bt_collection as $bt_key => $bt_name) {
$body_types[] = [
'id' => $bt_key,
'name' => $bt_name,
];
}
return new ApiResponse(true, '', [
'body_types' => $body_types,
]);
}
protected function getLineType($mv_type_id, $vehicle_use_type, $is_public = false)
{
$line = '';

View file

@ -4,18 +4,23 @@ namespace App\Controller\CustomerAppAPI;
use Symfony\Component\HttpFoundation\Request;
use Catalyst\ApiBundle\Component\Response as ApiResponse;
use CrEOF\Spatial\PHP\Types\Geometry\Point;
use App\Service\InvoiceGeneratorInterface;
use App\Service\PriceTierManager;
use App\Ramcar\InvoiceCriteria;
use App\Ramcar\TradeInType;
use App\Ramcar\TransactionOrigin;
use App\Entity\CustomerVehicle;
use App\Entity\Promo;
use App\Entity\Battery;
use App\Entity\BatterySize;
use App\Entity\Customer;
use App\Entity\CustomerMetadata;
class InvoiceController extends ApiController
{
public function getEstimate(Request $req, InvoiceGeneratorInterface $ic)
public function getEstimate(Request $req, InvoiceGeneratorInterface $ic, PriceTierManager $pt_manager)
{
// $this->debugRequest($req);
@ -36,6 +41,18 @@ class InvoiceController extends ApiController
return new ApiResponse(false, 'No customer information found.');
}
// get customer location from customer_metadata using customer id
$lng = $req->request->get('longitude');
$lat = $req->request->get('latitude');
if ((empty($lng)) || (empty($lat)))
{
// use customer metadata location as basis
$coordinates = $this->getCustomerMetadata($cust);
}
else
$coordinates = new Point($lng, $lat);
// make invoice criteria
$icrit = new InvoiceCriteria();
$icrit->setServiceType($req->request->get('service_type'));
@ -103,7 +120,8 @@ class InvoiceController extends ApiController
if (!empty($trade_in_type) && !empty($trade_in_batt)) {
$ti_batt_obj = $this->em->getRepository(Battery::class)->find($trade_in_batt);
if (!empty($ti_batt_obj)) {
$icrit->addEntry($ti_batt_obj, $trade_in_type, 1);
$ti_batt_size_obj = $ti_batt_obj->getSize();
$icrit->addTradeInEntry($ti_batt_size_obj, $trade_in_type, 1);
}
}
@ -113,6 +131,18 @@ class InvoiceController extends ApiController
// set JO source
$icrit->setSource(TransactionOrigin::MOBILE_APP);
// set price tier
$pt_id = 0;
if ($coordinates != null)
{
error_log('coordinates are not null');
$pt_id = $pt_manager->getPriceTier($coordinates);
}
else
error_log('null?');
$icrit->setPriceTier($pt_id);
// send to invoice generator
$invoice = $ic->generateInvoice($icrit);
@ -148,4 +178,28 @@ class InvoiceController extends ApiController
// response
return new ApiResponse(true, '', $data);
}
protected function getCustomerMetadata(Customer $cust)
{
$coordinates = null;
// check if customer already has existing metadata
$c_meta = $this->em->getRepository(CustomerMetadata::class)->findOneBy(['customer' => $cust]);
if ($c_meta != null)
{
$meta_data = $c_meta->getAllMetaInfo();
foreach ($meta_data as $m_info)
{
if ((isset($m_info['longitude'])) && (isset($m_info['latitude'])))
{
$lng = $m_info['longitude'];
$lat = $m_info['latitude'];
$coordinates = new Point($lng, $lat);
}
}
}
return $coordinates;
}
}

View file

@ -21,6 +21,7 @@ use App\Service\HubDistributor;
use App\Service\HubFilterLogger;
use App\Service\HubFilteringGeoChecker;
use App\Service\JobOrderManager;
use App\Service\PriceTierManager;
use App\Ramcar\ServiceType;
use App\Ramcar\APIRiderStatus;
use App\Ramcar\InvoiceCriteria;
@ -34,6 +35,7 @@ use App\Ramcar\WarrantyClass;
use App\Ramcar\HubCriteria;
use App\Ramcar\DeliveryStatus;
use App\Entity\Battery;
use App\Entity\BatterySize;
use App\Entity\Hub;
use App\Entity\Promo;
use App\Entity\JOEvent;
@ -484,8 +486,11 @@ class JobOrderController extends ApiController
HubDistributor $hub_dist,
HubFilterLogger $hub_filter_logger,
HubFilteringGeoChecker $hub_geofence,
JobOrderManager $jo_manager
JobOrderManager $jo_manager,
PriceTierManager $pt_manager
) {
//error_log("CREATING JOB ORDER WITH PARAMS " . print_r($req->request->all(), true));
// validate params
$validity = $this->validateRequest($req, [
'service_type',
@ -573,6 +578,8 @@ class JobOrderController extends ApiController
$flag_advance_order = true;
// $flag_advance_order = $advance_order ? true : false;
//error_log("RUNNING QUERY NEXT");
$jo = new JobOrder();
$jo->setSource(TransactionOrigin::MOBILE_APP)
->setStatus(JOStatus::PENDING)
@ -641,6 +648,8 @@ class JobOrderController extends ApiController
$icrit->addPromo($promo);
}
//error_log("CONTINUING QUERY BUILDING");
// check customer vehicle
$cv = $this->em->getRepository(CustomerVehicle::class)->find($req->request->get('cv_id'));
if ($cv == null) {
@ -688,7 +697,8 @@ class JobOrderController extends ApiController
if (!empty($trade_in_type) && !empty($trade_in_batt)) {
$ti_batt_obj = $this->em->getRepository(Battery::class)->find($trade_in_batt);
if (!empty($ti_batt_obj)) {
$icrit->addEntry($ti_batt_obj, $trade_in_type, 1);
$ti_batt_size_obj = $ti_batt_obj->getSize();
$icrit->addTradeInEntry($ti_batt_size_obj, $trade_in_type, 1);
}
}
@ -698,17 +708,38 @@ class JobOrderController extends ApiController
// set JO source
$icrit->setSource(TransactionOrigin::MOBILE_APP);
// set price tier
$pt_id = $pt_manager->getPriceTier($jo->getCoordinates());
$icrit->setPriceTier($pt_id);
// send to invoice generator
$invoice = $ic->generateInvoice($icrit);
$jo->setInvoice($invoice);
//error_log("GENERATED INVOICE");
// save here first so we have a JO ID which is required for the hub selector
$this->em->persist($invoice);
$this->em->persist($jo);
$this->em->flush();
// assign hub and rider
// check if hub is null
if ($hub == null) {
//error_log("NO HUB");
// TODO: need to factor out the setting of HubCriteria fields
$hub_criteria = new HubCriteria();
$hub_criteria->setPoint($jo->getCoordinates());
// set job order info
$hub_criteria->setJobOrderId($jo->getID())
->setJoType($jo->getServiceType())
->setJoOrigin($jo->getSource())
->setCustomerClass($cust->getCustomerClassification())
->setOrderDate($jo->getDateCreate())
->setServiceType($jo->getServiceType());
// get distance limit for mobile from env
// get value of hub_filter_enable from env
$limit_distance = $_ENV['CUST_DISTANCE_LIMIT'];
@ -746,6 +777,10 @@ class JobOrderController extends ApiController
$hub_criteria->setCustomerId($customer_id);
// set filter flags for inventory and available riders
$hub_criteria->setInventoryCheck();
$hub_criteria->setRidersCheck();
// find nearest hubs
$nearest_hubs = $hub_select->find($hub_criteria);
@ -824,6 +859,8 @@ class JobOrderController extends ApiController
}
}
} else {
//error_log("HAS HUB: " . $hub->getID());
$jo->setHub($hub);
$jo->setStatus(JOStatus::RIDER_ASSIGN);
$jo->setStatusAutoAssign(AutoAssignStatus::HUB_ASSIGNED);
@ -835,8 +872,10 @@ class JobOrderController extends ApiController
$hub_dist->incrementJoCountForHub($hub);
}
//error_log("DONE SELECTING HUB");
// save additional hub related changes
$this->em->persist($jo);
$this->em->persist($invoice);
// add event log for JO
$event = new JOEvent();
@ -947,6 +986,8 @@ class JobOrderController extends ApiController
}
}
//error_log("DONE CREATING JOB ORDER " . $jo->getID());
// response
return new ApiResponse(true, '', [
'jo_id' => $jo->getID(),
@ -970,7 +1011,8 @@ class JobOrderController extends ApiController
HubDistributor $hub_dist,
HubFilterLogger $hub_filter_logger,
HubFilteringGeoChecker $hub_geofence,
JobOrderManager $jo_manager
JobOrderManager $jo_manager,
PriceTierManager $pt_manager
) {
// validate params
$validity = $this->validateRequest($req, [
@ -1117,7 +1159,8 @@ class JobOrderController extends ApiController
if (!empty($trade_in_type) && !empty($trade_in_batt)) {
$ti_batt_obj = $this->em->getRepository(Battery::class)->find($trade_in_batt);
if (!empty($ti_batt_obj)) {
$icrit->addEntry($ti_batt_obj, $trade_in_type, 1);
$battery_size = $ti_batt_obj->getSize();
$icrit->addTradeInEntry($battery_size, $trade_in_type, 1);
}
}
@ -1127,6 +1170,10 @@ class JobOrderController extends ApiController
// set JO source
$icrit->setSource(TransactionOrigin::MOBILE_APP);
// set price tier
$pt_id = $pt_manager->getPriceTier($jo->getCoordinates());
$icrit->setPriceTier($pt_id);
// send to invoice generator
$invoice = $ic->generateInvoice($icrit);
$jo->setInvoice($invoice);

View file

@ -4,16 +4,19 @@ namespace App\Controller\CustomerAppAPI;
use Symfony\Component\HttpFoundation\Request;
use Catalyst\ApiBundle\Component\Response as ApiResponse;
use CrEOF\Spatial\PHP\Types\Geometry\Point;
use App\Entity\CustomerVehicle;
use App\Entity\JobOrder;
use App\Entity\VehicleManufacturer;
use App\Entity\Vehicle;
use App\Entity\ItemType;
use App\Ramcar\JOStatus;
use App\Ramcar\ServiceType;
use App\Ramcar\TradeInType;
use App\Ramcar\InsuranceApplicationStatus;
use App\Service\PayMongoConnector;
use App\Service\PriceTierManager;
use DateTime;
class VehicleController extends ApiController
@ -237,7 +240,7 @@ class VehicleController extends ApiController
]);
}
public function getCompatibleBatteries(Request $req, $vid)
public function getCompatibleBatteries(Request $req, $vid, PriceTierManager $pt_manager)
{
// validate params
$validity = $this->validateRequest($req);
@ -252,11 +255,43 @@ class VehicleController extends ApiController
return new ApiResponse(false, 'Invalid vehicle.');
}
// get location from request
$lng = $req->query->get('longitude', '');
$lat = $req->query->get('latitude', '');
$batts = $vehicle->getActiveBatteries();
$pt_id = 0;
if ((!(empty($lng))) && (!(empty($lat))))
{
// get the price tier
$coordinates = new Point($lng, $lat);
$pt_id = $pt_manager->getPriceTier($coordinates);
}
// batteries
$batt_list = [];
$batts = $vehicle->getActiveBatteries();
foreach ($batts as $batt) {
// TODO: Add warranty_tnv to battery information
// check if customer location is in a price tier location
if ($pt_id == 0)
$price = $batt->getSellingPrice();
else
{
// get item type for battery
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'battery']);
if ($item_type == null)
$price = $batt->getSellingPrice();
else
{
$item_type_id = $item_type->getID();
$batt_id = $batt->getID();
// find the item price given price tier id and battery id
$price = $pt_manager->getItemPrice($pt_id, $item_type_id, $batt_id);
}
}
$batt_list[] = [
'id' => $batt->getID(),
'mfg_id' => $batt->getManufacturer()->getID(),
@ -265,7 +300,7 @@ class VehicleController extends ApiController
'model_name' => $batt->getModel()->getName(),
'size_id' => $batt->getSize()->getID(),
'size_name' => $batt->getSize()->getName(),
'price' => $batt->getSellingPrice(),
'price' => $price,
'wty_private' => $batt->getWarrantyPrivate(),
'wty_commercial' => $batt->getWarrantyCommercial(),
'image_url' => $this->getBatteryImageURL($req, $batt),

View file

@ -4,6 +4,7 @@ namespace App\Controller;
use App\Ramcar\InsuranceApplicationStatus;
use App\Service\FCMSender;
use App\Service\InsuranceConnector;
use App\Entity\InsuranceApplication;
use Doctrine\ORM\EntityManagerInterface;
@ -15,11 +16,13 @@ use DateTime;
class InsuranceController extends Controller
{
protected $ic;
protected $em;
protected $fcmclient;
public function __construct(EntityManagerInterface $em, FCMSender $fcmclient)
public function __construct(InsuranceConnector $ic, EntityManagerInterface $em, FCMSender $fcmclient)
{
$this->ic = $ic;
$this->em = $em;
$this->fcmclient = $fcmclient;
}
@ -28,17 +31,8 @@ class InsuranceController extends Controller
{
$payload = $req->request->all();
// DEBUG
@file_put_contents(__DIR__ . '/../../var/log/insurance.log', print_r($payload, true) . "\r\n----------------------------------------\r\n\r\n", FILE_APPEND);
error_log(print_r($payload, true));
/*
return $this->json([
'success' => true,
]);
*/
// END DEBUG
// log this callback
$this->ic->log('CALLBACK', "[]", json_encode($payload), 'callback');
// if no transaction code given, silently fail
if (empty($payload['transaction_code'])) {

View file

@ -0,0 +1,269 @@
<?php
namespace App\Controller;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Catalyst\MenuBundle\Annotation\Menu;
use App\Entity\PriceTier;
use App\Entity\Battery;
use App\Entity\ServiceOffering;
use App\Entity\ItemType;
use App\Entity\ItemPrice;
class ItemPricingController extends Controller
{
/**
* @Menu(selected="item_pricing")
* @IsGranted("item_pricing.update")
*/
public function index (EntityManagerInterface $em)
{
// get all the price tiers
$price_tiers = $em->getRepository(PriceTier::class)->findAll();
// get all item types
$item_types = $em->getRepository(ItemType::class)->findBy([], ['name' => 'asc']);
// get all the items/batteries
// load only batteries upon initial loading
$items = $this->getBatteries($em);
// set the default item type to battery
$default_it = $em->getRepository(ItemType::class)->findOneBy(['code' => 'battery']);
$params = [
'sets' => [
'price_tiers' => $price_tiers,
'item_types' => $item_types,
],
'items' => $items,
'default_item_type_id' => $default_it->getID(),
];
return $this->render('item-pricing/form.html.twig', $params);
}
/**
* @Menu(selected="item_pricing")
* @IsGranted("item_pricing.update")
*/
public function formSubmit(Request $req, EntityManagerInterface $em)
{
$pt_id = $req->request->get('price_tier_id');
$it_id = $req->request->get('item_type_id');
$prices = $req->request->get('price');
// get the item type
$item_type = $em->getRepository(ItemType::class)->find($it_id);
if ($item_type->getCode() == 'battery')
{
// get batteries
$items = $em->getRepository(Battery::class)->findBy(['flag_active' => true], ['id' => 'asc']);
}
else
{
// get service offerings
$items = $em->getRepository(ServiceOffering::class)->findBy([], ['id' => 'asc']);
}
// on default price tier
if ($pt_id == 0)
{
// default price tier, update battery or service offering, depending on item type
// NOTE: battery and service offering prices or fees are stored as decimal.
foreach ($items as $item)
{
$item_id = $item->getID();
if (isset($prices[$item_id]))
{
// check item type
if ($item_type->getCode() == 'battery')
$item->setSellingPrice($prices[$item_id]);
else
$item->setFee($prices[$item_id]);
}
}
}
else
{
// get the price tier
$price_tier = $em->getRepository(PriceTier::class)->find($pt_id);
$item_prices = $price_tier->getItemPrices();
// clear the tier's item prices for the specific item type
foreach ($item_prices as $ip)
{
if ($ip->getItemType() == $item_type)
$em->remove($ip);
}
// update the tier's item prices
foreach ($items as $item)
{
$item_id = $item->getID();
$item_price = new ItemPrice();
$item_price->setItemType($item_type)
->setPriceTier($price_tier)
->setItemID($item_id);
if (isset($prices[$item_id]))
{
$item_price->setPrice($prices[$item_id] * 100);
}
else
{
$item_price->setPrice($item->getPrice() * 100);
}
// save
$em->persist($item_price);
}
}
$em->flush();
return $this->redirectToRoute('item_pricing');
}
/**
* @IsGranted("item_pricing.update")
*/
public function itemPrices(EntityManagerInterface $em, $pt_id, $it_id)
{
$pt_prices = [];
// get the item type
$it = $em->getRepository(ItemType::class)->find($it_id);
// check if default prices are needed
if ($pt_id != 0)
{
// get the price tier
$pt = $em->getRepository(PriceTier::class)->find($pt_id);
// get the items under the price tier
$pt_items = $pt->getItemPrices();
foreach ($pt_items as $pt_item)
{
// make item price hash
$pt_prices[$pt_item->getItemID()] = $pt_item->getPrice();
}
}
// get the prices from battery or service offering, depending on item type
if ($it->getCode() == 'battery')
{
// get batteries
$items = $em->getRepository(Battery::class)->findBy(['flag_active' => true], ['id' => 'asc']);
}
else
{
// get service offerings
$items = $em->getRepository(ServiceOffering::class)->findBy([], ['id' => 'asc']);
}
$data_items = [];
foreach ($items as $item)
{
$item_id = $item->getID();
// get default price
if ($it->getCode() == 'battery')
{
$price = $item->getSellingPrice();
$name = $item->getModel()->getName() . ' ' . $item->getSize()->getName();
}
else
{
$price = $item->getFee();
$name = $item->getName();
}
// check if tier has price for item
if (isset($pt_prices[$item_id]))
{
$pt_price = $pt_prices[$item_id];
// actual price
$price = number_format($pt_price / 100, 2, '.', '');
}
$actual_price = $price;
$data_items[] = [
'id' => $item_id,
'name' => $name,
'item_type_id' => $it->getID(),
'item_type' => $it->getName(),
'price' => $actual_price,
];
}
// response
return new JsonResponse([
'items' => $data_items,
]);
}
protected function getBatteries(EntityManagerInterface $em)
{
// get the item type for battery
$batt_item_type = $em->getRepository(ItemType::class)->findOneBy(['code' => 'battery']);
// get all active batteries
$batts = $em->getRepository(Battery::class)->findBy(['flag_active' => true], ['id' => 'asc']);
foreach ($batts as $batt)
{
$batt_set[$batt->getID()] = [
'name' => $batt->getModel()->getName() . ' ' . $batt->getSize()->getName(),
'item_type_id' => $batt_item_type->getID(),
'item_type' => $batt_item_type->getName(),
'price' => $batt->getSellingPrice(),
];
}
return [
'items' => $batt_set,
];
}
protected function getServiceOfferings(EntityeManagerInterface $em)
{
// get the item type for service offering
$service_item_type = $em->getRepository(ItemType::class)->findOneBy(['code' => 'service_offering']);
// get all service offerings
$services = $em->getRepository(ServiceOffering::class)->findBy([], ['id' => 'asc']);
$service_set = [];
foreach ($services as $service)
{
$service_set[$service->getID()] = [
'name' => $service->getName(),
'item_type_id' => $service_item_type->getID(),
'item_type' => $service_item_type->getName(),
'price' => $service->getFee(),
];
}
return [
'items' => $service_set,
];
}
}

View file

@ -0,0 +1,251 @@
<?php
namespace App\Controller;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use App\Entity\ItemType;
use Catalyst\MenuBundle\Annotation\Menu;
class ItemTypeController extends Controller
{
/**
* @Menu(selected="item_type_list")
* @IsGranted("item_type.list")
*/
public function index ()
{
return $this->render('item-type/list.html.twig');
}
/**
* @IsGranted("item_type.list")
*/
public function datatableRows(Request $req)
{
// get query builder
$qb = $this->getDoctrine()
->getRepository(ItemType::class)
->createQueryBuilder('q');
// get datatable params
$datatable = $req->request->get('datatable');
// count total records
$tquery = $qb->select('COUNT(q)');
$this->setQueryFilters($datatable, $tquery);
$total = $tquery->getQuery()
->getSingleScalarResult();
// get current page number
$page = $datatable['pagination']['page'] ?? 1;
$perpage = $datatable['pagination']['perpage'];
$offset = ($page - 1) * $perpage;
// add metadata
$meta = [
'page' => $page,
'perpage' => $perpage,
'pages' => ceil($total / $perpage),
'total' => $total,
'sort' => 'asc',
'field' => 'id'
];
// build query
$query = $qb->select('q');
$this->setQueryFilters($datatable, $query);
// check if sorting is present, otherwise use default
if (isset($datatable['sort']['field']) && !empty($datatable['sort']['field'])) {
$order = $datatable['sort']['sort'] ?? 'asc';
$query->orderBy('q.' . $datatable['sort']['field'], $order);
} else {
$query->orderBy('q.id', 'asc');
}
// get rows for this page
$obj_rows = $query->setFirstResult($offset)
->setMaxResults($perpage)
->getQuery()
->getResult();
// process rows
$rows = [];
foreach ($obj_rows as $orow) {
// add row data
$row['id'] = $orow->getID();
$row['name'] = $orow->getName();
// add row metadata
$row['meta'] = [
'update_url' => '',
'delete_url' => ''
];
// add crud urls
if ($this->isGranted('item_type.update'))
$row['meta']['update_url'] = $this->generateUrl('item_type_update_form', ['id' => $row['id']]);
if ($this->isGranted('item_type.delete'))
$row['meta']['delete_url'] = $this->generateUrl('item_type_delete', ['id' => $row['id']]);
$rows[] = $row;
}
// response
return $this->json([
'meta' => $meta,
'data' => $rows
]);
}
/**
* @Menu(selected="item_type.list")
* @IsGranted("item_type.add")
*/
public function addForm()
{
$item_type = new ItemType();
$params = [
'obj' => $item_type,
'mode' => 'create',
];
// response
return $this->render('item-type/form.html.twig', $params);
}
/**
* @IsGranted("item_type.add")
*/
public function addSubmit(Request $req, EntityManagerInterface $em, ValidatorInterface $validator)
{
$item_type = new ItemType();
$this->setObject($item_type, $req);
// validate
$errors = $validator->validate($item_type);
// initialize error list
$error_array = [];
// add errors to list
foreach ($errors as $error) {
$error_array[$error->getPropertyPath()] = $error->getMessage();
}
// check if any errors were found
if (!empty($error_array)) {
// return validation failure response
return $this->json([
'success' => false,
'errors' => $error_array
], 422);
}
// validated! save the entity
$em->persist($item_type);
$em->flush();
// return successful response
return $this->json([
'success' => 'Changes have been saved!'
]);
}
/**
* @Menu(selected="item_type_list")
* @ParamConverter("item_type", class="App\Entity\ItemType")
* @IsGranted("item_type.update")
*/
public function updateForm($id, EntityManagerInterface $em, ItemType $item_type)
{
$params = [];
$params['obj'] = $item_type;
$params['mode'] = 'update';
// response
return $this->render('item-type/form.html.twig', $params);
}
/**
* @ParamConverter("item_type", class="App\Entity\ItemType")
* @IsGranted("item_type.update")
*/
public function updateSubmit(Request $req, EntityManagerInterface $em, ValidatorInterface $validator, ItemType $item_type)
{
$this->setObject($item_type, $req);
// validate
$errors = $validator->validate($item_type);
// initialize error list
$error_array = [];
// add errors to list
foreach ($errors as $error) {
$error_array[$error->getPropertyPath()] = $error->getMessage();
}
// check if any errors were found
if (!empty($error_array)) {
// return validation failure response
return $this->json([
'success' => false,
'errors' => $error_array
], 422);
}
// validated! save the entity
$em->flush();
// return successful response
return $this->json([
'success' => 'Changes have been saved!'
]);
}
/**
* @ParamConverter("item_type", class="App\Entity\ItemType")
* @IsGranted("item_type.delete")
*/
public function deleteSubmit(EntityManagerInterface $em, ItemType $item_type)
{
// delete this object
$em->remove($item_type);
$em->flush();
// response
$response = new Response();
$response->setStatusCode(Response::HTTP_OK);
$response->send();
}
protected function setObject(ItemType $obj, Request $req)
{
// set and save values
$obj->setName($req->request->get('name'))
->setCode($req->request->get('code'));
}
protected function setQueryFilters($datatable, QueryBuilder $query)
{
if (isset($datatable['query']['data-rows-search']) && !empty($datatable['query']['data-rows-search'])) {
$query->where('q.name LIKE :filter')
->setParameter('filter', '%' . $datatable['query']['data-rows-search'] . '%');
}
}
}

View file

@ -30,6 +30,7 @@ use App\Service\HubSelector;
use App\Service\RiderTracker;
use App\Service\MotivConnector;
use App\Service\PriceTierManager;
use App\Service\GeofenceTracker;
use Symfony\Component\HttpFoundation\Request;
@ -42,6 +43,8 @@ use Doctrine\ORM\EntityManagerInterface;
use Catalyst\MenuBundle\Annotation\Menu;
use CrEOF\Spatial\PHP\Types\Geometry\Point;
class JobOrderController extends Controller
{
public function getJobOrders(Request $req, JobOrderHandlerInterface $jo_handler)
@ -741,7 +744,7 @@ class JobOrderController extends Controller
}
public function generateInvoice(Request $req, InvoiceGeneratorInterface $ic)
public function generateInvoice(Request $req, InvoiceGeneratorInterface $ic, PriceTierManager $pt_manager)
{
// error_log('generating invoice...');
$error = false;
@ -751,6 +754,21 @@ class JobOrderController extends Controller
$promo_id = $req->request->get('promo');
$cvid = $req->request->get('cvid');
$service_charges = $req->request->get('service_charges', []);
$flag_coolant = $req->request->get('flag_coolant', false);
$flag_sealant = $req->request->get('flag_sealant', false);
// coordinates
// need to check if lng and lat are set
$lng = $req->request->get('coord_lng', 0);
$lat = $req->request->get('coord_lat', 0);
$price_tier = 0;
if (!empty($lng) && !empty($lat))
{
$coordinates = new Point($req->request->get('coord_lng'), $req->request->get('coord_lat'));
$price_tier = $pt_manager->getPriceTier($coordinates);
}
$em = $this->getDoctrine()->getManager();
@ -767,8 +785,10 @@ class JobOrderController extends Controller
$criteria->setServiceType($stype)
->setCustomerVehicle($cv)
->setIsTaxable()
->setSource(TransactionOrigin::CALL);
->setSource(TransactionOrigin::CALL)
->setPriceTier($price_tier)
->setHasCoolant($flag_coolant)
->setHasSealant($flag_sealant);
/*
// if it's a jumpstart or troubleshoot only, we know what to charge already

View file

@ -4,18 +4,23 @@ namespace App\Controller;
use App\Entity\GatewayTransaction;
use App\Ramcar\TransactionStatus;
use App\Service\PayMongoConnector;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use DateTime;
class PayMongoController extends Controller
{
protected $pm;
protected $em;
public function __construct(EntityManagerInterface $em)
public function __construct(PayMongoConnector $pm, EntityManagerInterface $em)
{
$this->pm = $pm;
$this->em = $em;
}
@ -23,16 +28,8 @@ class PayMongoController extends Controller
{
$payload = json_decode($req->getContent(), true);
// DEBUG
@file_put_contents(__DIR__ . '/../../var/log/paymongo.log', print_r($payload, true) . "\r\n----------------------------------------\r\n\r\n", FILE_APPEND);
/*
return $this->json([
'success' => true,
]);
*/
// END DEBUG
// log this callback
$this->pm->log('CALLBACK', "[]", $req->getContent(), 'callback');
// if no event type given, silently fail
if (empty($payload['data'])) {
@ -50,10 +47,8 @@ class PayMongoController extends Controller
switch ($event_name) {
case "payment.paid":
return $this->handlePaymentPaid($event);
break;
case "payment.failed":
return $this->handlePaymentPaid($event);
break;
return $this->handlePaymentFailed($event);
case "payment.refunded": // TODO: handle refunds
case "payment.refund.updated":
case "checkout_session.payment.paid":
@ -74,6 +69,7 @@ class PayMongoController extends Controller
if (!empty($obj)) {
// mark as paid
$obj->setStatus(TransactionStatus::PAID);
$obj->setDatePay(new DateTime());
$this->em->flush();
}
@ -82,7 +78,7 @@ class PayMongoController extends Controller
]);
}
protected function handlePaymentFailed(Request $req)
protected function handlePaymentFailed($event)
{
// TODO: do something about failed payments?
return $this->json([

View file

@ -0,0 +1,355 @@
<?php
namespace App\Controller;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Catalyst\MenuBundle\Annotation\Menu;
use App\Entity\PriceTier;
use App\Entity\SupportedArea;
class PriceTierController extends Controller
{
/**
* @Menu(selected="price_tier_list")
* @IsGranted("price_tier.list")
*/
public function index()
{
return $this->render('price-tier/list.html.twig');
}
/**
* @IsGranted("price_tier.list")
*/
public function datatableRows(Request $req)
{
// get query builder
$qb = $this->getDoctrine()
->getRepository(PriceTier::class)
->createQueryBuilder('q');
// get datatable params
$datatable = $req->request->get('datatable');
// count total records
$tquery = $qb->select('COUNT(q)');
$this->setQueryFilters($datatable, $tquery);
$total = $tquery->getQuery()
->getSingleScalarResult();
// get current page number
$page = $datatable['pagination']['page'] ?? 1;
$perpage = $datatable['pagination']['perpage'];
$offset = ($page - 1) * $perpage;
// add metadata
$meta = [
'page' => $page,
'perpage' => $perpage,
'pages' => ceil($total / $perpage),
'total' => $total,
'sort' => 'asc',
'field' => 'id'
];
// build query
$query = $qb->select('q');
$this->setQueryFilters($datatable, $query);
// check if sorting is present, otherwise use default
if (isset($datatable['sort']['field']) && !empty($datatable['sort']['field'])) {
$order = $datatable['sort']['sort'] ?? 'asc';
$query->orderBy('q.' . $datatable['sort']['field'], $order);
} else {
$query->orderBy('q.id', 'asc');
}
// get rows for this page
$obj_rows = $query->setFirstResult($offset)
->setMaxResults($perpage)
->getQuery()
->getResult();
// process rows
$rows = [];
foreach ($obj_rows as $orow) {
// add row data
$row['id'] = $orow->getID();
$row['name'] = $orow->getName();
// add row metadata
$row['meta'] = [
'update_url' => '',
'delete_url' => ''
];
// add crud urls
if ($this->isGranted('price_tier.update'))
$row['meta']['update_url'] = $this->generateUrl('price_tier_update_form', ['id' => $row['id']]);
if ($this->isGranted('service_offering.delete'))
$row['meta']['delete_url'] = $this->generateUrl('price_tier_delete', ['id' => $row['id']]);
$rows[] = $row;
}
// response
return $this->json([
'meta' => $meta,
'data' => $rows
]);
}
/**
* @Menu(selected="price_tier.list")
* @IsGranted("price_tier.add")
*/
public function addForm(EntityManagerInterface $em)
{
$pt = new PriceTier();
// get the supported areas
$sets = $this->generateFormSets($em);
$params = [
'obj' => $pt,
'sets' => $sets,
'mode' => 'create',
];
// response
return $this->render('price-tier/form.html.twig', $params);
}
/**
* @IsGranted("price_tier.add")
*/
public function addSubmit(Request $req, EntityManagerInterface $em, ValidatorInterface $validator)
{
// initialize error list
$error_array = [];
$pt = new PriceTier();
$error_array = $this->validateRequest($em, $req);
$this->setObject($pt, $req);
// validate
$errors = $validator->validate($pt);
// add errors to list
foreach ($errors as $error) {
$error_array[$error->getPropertyPath()] = $error->getMessage();
}
// check if any errors were found
if (!empty($error_array)) {
// return validation failure response
return $this->json([
'success' => false,
'errors' => $error_array
], 422);
}
// validated! save the entity
$em->persist($pt);
// set the price tier id for the selected supported areas
$this->updateSupportedAreas($em, $pt, $req);
$em->flush();
// return successful response
return $this->json([
'success' => 'Changes have been saved!'
]);
}
/**
* @Menu(selected="price_tier_list")
* @ParamConverter("pt", class="App\Entity\PriceTier")
* @IsGranted("price_tier.update")
*/
public function updateForm($id, EntityManagerInterface $em, PriceTier $pt)
{
// get the supported areas
$sets = $this->generateFormSets($em, $pt);
$params = [
'obj' => $pt,
'sets' => $sets,
'mode' => 'update',
];
// response
return $this->render('price-tier/form.html.twig', $params);
}
/**
* @ParamConverter("pt", class="App\Entity\PriceTier")
* @IsGranted("price_tier.update")
*/
public function updateSubmit(Request $req, EntityManagerInterface $em, ValidatorInterface $validator, PriceTier $pt)
{
// initialize error list
$error_array = [];
// clear supported areas of price tier
$this->clearPriceTierSupportedAreas($em, $pt);
$error_array = $this->validateRequest($em, $req);
$this->setObject($pt, $req);
// validate
$errors = $validator->validate($pt);
// add errors to list
foreach ($errors as $error) {
$error_array[$error->getPropertyPath()] = $error->getMessage();
}
// check if any errors were found
if (!empty($error_array)) {
// return validation failure response
return $this->json([
'success' => false,
'errors' => $error_array
], 422);
}
// set the price tier id for the selected supported areas
$this->updateSupportedAreas($em, $pt, $req);
// validated! save the entity
$em->flush();
// return successful response
return $this->json([
'success' => 'Changes have been saved!'
]);
}
/**
* @ParamConverter("pt", class="App\Entity\PriceTier")
* @IsGranted("price_tier.delete")
*/
public function deleteSubmit(EntityManagerInterface $em, PriceTier $pt)
{
// clear supported areas of price tier
$this->clearPriceTierSupportedAreas($em, $pt);
// delete this object
$em->remove($pt);
$em->flush();
// response
$response = new Response();
$response->setStatusCode(Response::HTTP_OK);
$response->send();
}
protected function validateRequest(EntityManagerInterface $em, Request $req)
{
// get areas
$areas = $req->request->get('areas');
// check if no areas selected aka empty
if (!empty($areas))
{
foreach ($areas as $area_id)
{
$supported_area = $em->getRepository(SupportedArea::class)->find($area_id);
if ($supported_area == null)
return ['areas' => 'Invalid area'];
// check if supported area already belongs to a price tier
if ($supported_area->getPriceTier() != null)
return ['areas' => 'Area already belongs to a price tier.'];
}
}
return null;
}
protected function setObject(PriceTier $obj, Request $req)
{
// clear supported areas first
$obj->clearSupportedAreas();
$obj->setName($req->request->get('name'));
}
protected function clearPriceTierSupportedAreas(EntityManagerInterface $em, PriceTier $obj)
{
// find the supported areas set with the price tier
$areas = $em->getRepository(SupportedArea::class)->findBy(['price_tier' => $obj]);
if (!empty($areas))
{
// set the price tier id for the supported areas to null
foreach ($areas as $area)
{
$area->setPriceTier(null);
}
$em->flush();
}
}
protected function updateSupportedAreas(EntityManagerInterface $em, PriceTier $obj, Request $req)
{
// get the selected areas
$areas = $req->request->get('areas');
// check if no areas selected aka empty
if (!empty($areas))
{
foreach ($areas as $area_id)
{
// get supported area
$supported_area = $em->getRepository(SupportedArea::class)->find($area_id);
if ($supported_area != null)
$supported_area->setPriceTier($obj);
}
}
}
protected function generateFormSets(EntityManagerInterface $em, PriceTier $pt = null)
{
// get the supported areas with no price tier id or price tier id is set to the one that is being updated
$areas = $em->getRepository(SupportedArea::class)->findBy(['price_tier' => array(null, $pt)]);
$areas_set = [];
foreach ($areas as $area)
{
$areas_set[$area->getID()] = $area->getName();
}
return [
'areas' => $areas_set
];
}
protected function setQueryFilters($datatable, QueryBuilder $query)
{
if (isset($datatable['query']['data-rows-search']) && !empty($datatable['query']['data-rows-search'])) {
$query->where('q.name LIKE :filter')
->setParameter('filter', '%' . $datatable['query']['data-rows-search'] . '%');
}
}
}

View file

@ -236,6 +236,10 @@ class ReportController extends Controller
$reason = $jor->getReason();
$dispatched_by = 'system';
if ($jor->getUser() != null)
$dispatched_by = $jor->getUser()->getFullName();
$res[] = [
$jo->getID(),
$jo->getDateSchedule()->format('m/d/Y H:i'),
@ -244,7 +248,7 @@ class ReportController extends Controller
JORejectionReason::getName($jor->getReason()),
$jor->getContactPerson(),
$jor->getRemarks(),
$jor->getUser()->getFullName(),
!empty($jor->getUser()) ? $jor->getUser()->getFullName() : "",
ServiceType::getName($jo->getServiceType()),
];
}

View file

@ -23,6 +23,7 @@ use App\Ramcar\JOStatus;
use App\Ramcar\ServiceType;
use App\Ramcar\WillingToWaitContent;
use App\Ramcar\CustomerClassification;
use App\Ramcar\CustomerNotWaitReason;
use DateTime;

View file

@ -13,6 +13,11 @@ use Catalyst\ApiBundle\Component\Response as APIResponse;
use App\Ramcar\APIResult;
use App\Entity\Vehicle;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
use CrEOF\Spatial\PHP\Types\Geometry\Point;
use Catalyst\AuthBundle\Service\ACLGenerator as ACLGenerator;
@ -25,7 +30,7 @@ class BatteryController extends ApiController
$this->acl_gen = $acl_gen;
}
public function getCompatibleBatteries(Request $req, $vid, EntityManagerInterface $em)
public function getCompatibleBatteries(Request $req, $vid, EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->denyAccessUnlessGranted('tapi_battery_compatible.list', null, 'No access.');
@ -43,13 +48,44 @@ class BatteryController extends ApiController
return new APIResponse(false, $message);
}
// get location from request
$lng = $req->request->get('longitude', '');
$lat = $req->request->get('latitude', '');
$batts = $vehicle->getActiveBatteries();
$pt_id = 0;
if ((!(empty($lng))) && (!(empty($lat))))
{
// get the price tier
$coordinates = new Point($lng, $lat);
$pt_id = $pt_manager->getPriceTier($coordinates);
}
// batteries
$batt_list = [];
// $batts = $vehicle->getBatteries();
$batts = $vehicle->getActiveBatteries();
foreach ($batts as $batt)
{
// TODO: Add warranty_tnv to battery information
// check if customer location is in a price tier location
if ($pt_id == 0)
$price = $batt->getSellingPrice();
else
{
// get item type for battery
$item_type = $em->getRepository(ItemType::class)->findOneBy(['code' => 'battery']);
if ($item_type == null)
$price = $batt->getSellingPrice();
else
{
$item_type_id = $item_type->getID();
$batt_id = $batt->getID();
// find the item price given price tier id and battery id
$price = $pt_manager->getItemPrice($pt_id, $item_type_id, $batt_id);
}
}
$batt_list[] = [
'id' => $batt->getID(),
'mfg_id' => $batt->getManufacturer()->getID(),
@ -58,7 +94,7 @@ class BatteryController extends ApiController
'model_name' => $batt->getModel()->getName(),
'size_id' => $batt->getSize()->getID(),
'size_name' => $batt->getSize()->getName(),
'price' => $batt->getSellingPrice(),
'price' => $price,
'wty_private' => $batt->getWarrantyPrivate(),
'wty_commercial' => $batt->getWarrantyCommercial(),
'image_url' => $this->getBatteryImageURL($req, $batt),

View file

@ -46,6 +46,7 @@ use App\Service\RiderTracker;
use App\Service\PromoLogger;
use App\Service\MapTools;
use App\Service\JobOrderManager;
use App\Service\PriceTierManager;
use App\Entity\JobOrder;
use App\Entity\CustomerVehicle;
@ -79,7 +80,8 @@ class JobOrderController extends ApiController
FCMSender $fcmclient,
RiderAssignmentHandlerInterface $rah, PromoLogger $promo_logger,
HubSelector $hub_select, HubDistributor $hub_dist, HubFilterLogger $hub_filter_logger,
HubFilteringGeoChecker $hub_geofence, EntityManagerInterface $em, JobOrderManager $jo_manager)
HubFilteringGeoChecker $hub_geofence, EntityManagerInterface $em, JobOrderManager $jo_manager,
PriceTierManager $pt_manager)
{
$this->denyAccessUnlessGranted('tapi_jo.request', null, 'No access.');
@ -165,7 +167,17 @@ class JobOrderController extends ApiController
// set JO source
$icrit->setSource(TransactionOrigin::THIRD_PARTY);
// set price tier
$pt_id = $pt_manager->getPriceTier($jo->getCoordinates());
$icrit->setPriceTier($pt_id);
// add the actual battery item first
$icrit->addEntry($data['batt'], null, 1);
// if we have a trade in, add it as well, assuming trade in battery == battery purchased
if (!empty($data['trade_in_type'])) {
$icrit->addEntry($data['batt'], $data['trade_in_type'], 1);
}
// send to invoice generator
$invoice = $ic->generateInvoice($icrit);
@ -227,6 +239,10 @@ class JobOrderController extends ApiController
// find nearest hubs
$nearest_hubs = $hub_select->find($hub_criteria);
// set filter flags for inventory and available riders
$hub_criteria->setInventoryCheck();
$hub_criteria->setRidersCheck();
if (!empty($nearest_hubs))
{
// go through the hub list, find the nearest hub

View file

@ -114,7 +114,9 @@ class Hub
public function getAvailableRiders()
{
$crit = Criteria::create();
$crit->where(Criteria::expr()->eq('flag_available', true));
$crit->where(Criteria::expr()->eq('flag_available', true))
->where(Criteria::expr()->eq('flag_active', true))
->where(Criteria::expr()->eq('current_job_order', null));
return $this->riders->matching($crit);
}

View file

@ -50,10 +50,24 @@ class InvoiceItem
*/
protected $battery;
// battery size for trade in items
/**
* @ORM\ManyToOne(targetEntity="BatterySize")
* @ORM\JoinColumn(name="battery_size_id", referencedColumnName="id")
*/
protected $battery_size;
// trade in type
/**
* @ORM\Column(type="string", length=20)
*/
protected $trade_in_type;
public function __construct()
{
$this->title = '';
$this->price = 0.0;
$this->trade_in_type = '';
}
public function getID()
@ -115,4 +129,26 @@ class InvoiceItem
{
return $this->battery;
}
public function setBatterySize(BatterySize $battery_size)
{
$this->battery_size = $battery_size;
return $this;
}
public function getBatterySize()
{
return $this->battery_size;
}
public function setTradeInType(string $trade_in_type)
{
$this->trade_in_type = $trade_in_type;
return $this;
}
public function getTradeInType()
{
return $this->trade_in_type;
}
}

97
src/Entity/ItemPrice.php Normal file
View file

@ -0,0 +1,97 @@
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="item_price")
*/
class ItemPrice
{
// unique id
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\ManyToOne(targetEntity="PriceTier", inversedBy="item_prices")
* @ORM\JoinColumn(name="price_tier_id", referencedColumnName="id")
*/
protected $price_tier;
// item type
/**
* @ORM\ManyToOne(targetEntity="ItemType", inversedBy="items")
* @ORM\JoinColumn(name="item_type_id", referencedColumnName="id")
*/
protected $item_type;
// could be battery id or service offering id, loosely coupled
/**
* @ORM\Column(type="integer")
*/
protected $item_id;
// current price
// NOTE: we need to move the decimal point two places to the left to get actual value
// we want to avoid floating point problems
/**
* @ORM\Column(type="integer")
*/
protected $price;
public function getID()
{
return $this->id;
}
public function setPriceTier(PriceTier $price_tier)
{
$this->price_tier = $price_tier;
return $this;
}
public function getPriceTier()
{
return $this->price_tier;
}
public function setItemType(ItemType $item_type)
{
$this->item_type = $item_type;
return $this;
}
public function getItemType()
{
return $this->item_type;
}
public function setItemID($item_id)
{
$this->item_id = $item_id;
return $this;
}
public function getItemID()
{
return $this->item_id;
}
public function setPrice($price)
{
$this->price = $price;
return $this;
}
public function getPrice()
{
return $this->price;
}
}

80
src/Entity/ItemType.php Normal file
View file

@ -0,0 +1,80 @@
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity
* @ORM\Table(name="item_type", indexes={
* @ORM\Index(name="item_type_idx", columns={"code"})
* })
*/
class ItemType
{
// unique id
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\Column(type="string", length=80)
* @Assert\NotBlank()
*/
protected $name;
/**
* @ORM\Column(type="string", length=80)
* @Assert\NotBlank()
*/
protected $code;
// items under an item type
/**
* @ORM\OneToMany(targetEntity="ItemPrice", mappedBy="item_type")
*/
protected $items;
public function __construct()
{
$this->code = '';
}
public function getID()
{
return $this->id;
}
public function setName($name)
{
$this->name = $name;
return $this;
}
public function getName()
{
return $this->name;
}
public function setCode($code)
{
$this->code = $code;
return $this;
}
public function getCode()
{
return $this->code;
}
public function getItems()
{
return $this->items;
}
}

View file

@ -441,6 +441,12 @@ class JobOrder
*/
protected $flag_cust_new;
// only for tire service, if it requires sealant or not
/**
* @ORM\Column(type="boolean")
*/
protected $flag_sealant;
public function __construct()
{
$this->date_create = new DateTime();
@ -458,6 +464,7 @@ class JobOrder
$this->trade_in_type = null;
$this->flag_rider_rating = false;
$this->flag_coolant = false;
$this->flag_sealant = false;
$this->priority = 0;
$this->meta = [];
@ -1256,4 +1263,15 @@ class JobOrder
return $this->flag_cust_new;
}
public function setHasSealant($flag = true)
{
$this->flag_sealant = $flag;
return $this;
}
public function hasSealant()
{
return $this->flag_sealant;
}
}

88
src/Entity/PriceTier.php Normal file
View file

@ -0,0 +1,88 @@
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @ORM\Entity
* @ORM\Table(name="price_tier")
*/
class PriceTier
{
// unique id
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
// name of price tier
/**
* @ORM\Column(type="string", length=80)
*/
protected $name;
// supported areas under price tier
/**
* @ORM\OneToMany(targetEntity="SupportedArea", mappedBy="price_tier");
*/
protected $supported_areas;
// items under a price tier
/**
* @ORM\OneToMany(targetEntity="ItemPrice", mappedBy="price_tier")
*/
protected $item_prices;
public function __construct()
{
$this->supported_areas = new ArrayCollection();
$this->items = new ArrayCollection();
}
public function getID()
{
return $this->id;
}
public function setName($name)
{
$this->name = $name;
return $this;
}
public function getName()
{
return $this->name;
}
public function getSupportedAreaObjects()
{
return $this->supported_areas;
}
public function getSupportedAreas()
{
$str_supported_areas = [];
foreach ($this->supported_areas as $supported_area)
$str_supported_areas[] = $supported_area->getID();
return $str_supported_areas;
}
public function clearSupportedAreas()
{
$this->supported_areas->clear();
return $this;
}
public function getItemPrices()
{
return $this->item_prices;
}
}

View file

@ -6,6 +6,7 @@ use Catalyst\AuthBundle\Entity\Role as BaseRole;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
/**
@ -16,6 +17,19 @@ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
*/
class Role extends BaseRole
{
/**
* @ORM\Id
* @ORM\Column(type="string", length=80)
* @Assert\NotBlank()
*/
protected $id;
/**
* @ORM\Column(type="string", length=80)
* @Assert\NotBlank()
*/
protected $name;
/**
* @ORM\ManyToMany(targetEntity="User", mappedBy="roles", fetch="EXTRA_LAZY")
*/

View file

@ -39,9 +39,24 @@ class SupportedArea
*/
protected $coverage_area;
// prevent certain hub filters from being used
/**
* @ORM\Column(type="json", nullable=true)
*/
protected $hub_filter_exceptions;
/**
* @ORM\ManyToOne(targetEntity="PriceTier", inversedBy="supported_areas")
* @ORM\JoinColumn(name="price_tier_id", referencedColumnName="id", nullable=true)
*/
protected $price_tier;
public function __construct()
{
$this->date_create = new DateTime();
$this->price_tier = null;
$this->hub_filter_exceptions = [];
}
public function getID()
@ -57,7 +72,7 @@ class SupportedArea
public function getDateCreate()
{
return $this->date_Create;
return $this->date_create;
}
public function setName($name)
@ -82,5 +97,27 @@ class SupportedArea
{
return $this->coverage_area;
}
public function setPriceTier(PriceTier $price_tier = null)
{
$this->price_tier = $price_tier;
return $this;
}
public function getPriceTier()
{
return $this->price_tier;
}
public function setHubFilterExceptions($exceptions)
{
$this->hub_filter_exceptions = $exceptions;
return $this;
}
public function getHubFilterExceptions()
{
return $this->hub_filter_exceptions;
}
}

View file

@ -58,18 +58,21 @@ class GatewayTransactionListener
'gateway_transaction' => $gt_obj,
]);
if (!empty($obj)) {
// make sure the object exists and has not been processed yet
if (!empty($obj) && $obj->getStatus() === InsuranceApplicationStatus::CREATED) {
// mark as paid
$obj->setDatePay(new DateTime());
$obj->setStatus(InsuranceApplicationStatus::PAID);
$this->em->flush();
}
// flag on api as paid
$result = $this->ic->tagApplicationPaid($obj->getID());
$result = $this->ic->tagApplicationPaid($obj->getExtTransactionId());
// something went wrong with insurance api
if (!$result['success'] || $result['response']['transaction_code'] !== 'GR004') {
error_log("INSURANCE MARK AS PAID FAILED FOR " . $obj->getID() . ": " . $result['error']['message']);
}
}
}
}

View file

@ -11,14 +11,19 @@ use App\Ramcar\TradeInType;
use App\Entity\Battery;
use App\Entity\ServiceOffering;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class BatteryReplacementWarranty implements InvoiceRuleInterface
{
protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->pt_manager = $pt_manager;
}
public function getID()
@ -29,6 +34,7 @@ class BatteryReplacementWarranty implements InvoiceRuleInterface
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$pt_id = $criteria->getPriceTier();
$items = [];
if ($stype == $this->getID())
@ -40,7 +46,14 @@ class BatteryReplacementWarranty implements InvoiceRuleInterface
{
$batt = $entry['battery'];
$qty = 1;
// check if price tier has item price
$pt_price = $this->getPriceTierItemPrice($pt_id);
if ($pt_price == null)
$price = $this->getServiceTypeFee();
else
$price = $pt_price;
$items[] = [
'service_type' => $this->getID(),
@ -104,19 +117,45 @@ class BatteryReplacementWarranty implements InvoiceRuleInterface
if ($qty < 1)
continue;
// if this is a trade in, add trade in
if (!empty($item['trade_in']) && TradeInType::validate($item['trade_in']))
$trade_in = $item['trade_in'];
else
if (empty($item['trade_in']))
{
$trade_in = null;
$criteria->addEntry($battery, $trade_in, $qty);
}
}
}
return null;
}
protected function getPriceTierItemPrice($pt_id)
{
// price_tier is default
if ($pt_id == 0)
return null;
// find the item type for service offering
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'service_offering']);
if ($item_type == null)
return null;
// find the service offering
$code = 'battery_replacement_warranty_fee';
$service = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
// check if service is null. If null, return null
if ($service == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $service->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getTitle($battery)
{
$title = $battery->getModel()->getName() . ' ' . $battery->getSize()->getName() . ' - Service Unit';

View file

@ -10,14 +10,19 @@ use App\Ramcar\TradeInType;
use App\Ramcar\ServiceType;
use App\Entity\Battery;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class BatterySales implements InvoiceRuleInterface
{
protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->pt_manager = $pt_manager;
}
public function getID()
@ -28,6 +33,7 @@ class BatterySales implements InvoiceRuleInterface
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$pt = $criteria->getPriceTier();
$items = [];
if ($stype == $this->getID())
@ -36,19 +42,28 @@ class BatterySales implements InvoiceRuleInterface
$entries = $criteria->getEntries();
foreach($entries as $entry)
{
$batt = $entry['battery'];
$qty = $entry['qty'];
$trade_in = null;
// check if entry is for trade in
if (isset($entry['trade_in']))
$trade_in = $entry['trade_in'];
$size = $batt->getSize();
// entry is a battery purchase
if ($trade_in == null)
{
// battery purchase
// safe to get entry with battery key since CRM and apps
// will set this for a battery purchase and trade_in will
// will not be set
$batt = $entry['battery'];
// check if price tier has item price for battery
$pt_price = $this->getPriceTierItemPrice($pt, $batt);
if ($pt_price == null)
$price = $batt->getSellingPrice();
else
$price = $pt_price;
$items[] = [
'service_type' => $this->getID(),
@ -87,6 +102,8 @@ class BatterySales implements InvoiceRuleInterface
{
// check if this is a valid battery
foreach ($invoice_items as $item)
{
if (isset($item['battery']))
{
$battery = $this->em->getRepository(Battery::class)->find($item['battery']);
@ -101,19 +118,34 @@ class BatterySales implements InvoiceRuleInterface
if ($qty < 1)
continue;
// if this is a trade in, add trade in
if (!empty($item['trade_in']) && TradeInType::validate($item['trade_in']))
$trade_in = $item['trade_in'];
else
$trade_in = null;
$criteria->addEntry($battery, $trade_in, $qty);
}
}
}
return null;
}
protected function getPriceTierItemPrice($pt_id, $batt)
{
// price tier is default
if ($pt_id == 0)
return null;
// find the item type battery
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'battery']);
if ($item_type == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $batt->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getTitle($battery)
{
$title = $battery->getModel()->getName() . ' ' . $battery->getSize()->getName();

View file

@ -11,14 +11,19 @@ use App\Ramcar\ServiceType;
use App\Entity\ServiceOffering;
use App\Entity\CustomerVehicle;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class Fuel implements InvoiceRuleInterface
{
protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->pt_manager = $pt_manager;
}
public function getID()
@ -29,6 +34,7 @@ class Fuel implements InvoiceRuleInterface
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$pt_id = $criteria->getPriceTier();
$items = [];
@ -36,7 +42,13 @@ class Fuel implements InvoiceRuleInterface
{
$cv = $criteria->getCustomerVehicle();
$fee = $this->getServiceTypeFee($cv);
// check if price tier has item price
$pt_price = $this->getPriceTierItemPrice($pt_id, $cv);
if ($pt_price == null)
$service_price = $this->getServiceTypeFee($cv);
else
$service_price = $pt_price;
$ftype = $cv->getFuelType();
@ -46,10 +58,10 @@ class Fuel implements InvoiceRuleInterface
'service_type' => $this->getID(),
'qty' => $qty,
'title' => $this->getServiceTitle($ftype),
'price' => $fee,
'price' => $service_price,
];
$qty_fee = bcmul($qty, $fee, 2);
$qty_fee = bcmul($qty, $service_price, 2);
$total_price = $qty_fee;
switch ($ftype)
@ -57,7 +69,15 @@ class Fuel implements InvoiceRuleInterface
case FuelType::GAS:
case FuelType::DIESEL:
$qty = 1;
// check if price tier has item price for fuel type
$pt_price = $this->getPriceTierFuelItemPrice($pt_id, $ftype);
if ($pt_price == null)
$price = $this->getFuelFee($ftype);
else
$price = $pt_price;
$items[] = [
'service_type' => $this->getID(),
'qty' => $qty,
@ -138,6 +158,70 @@ class Fuel implements InvoiceRuleInterface
return null;
}
protected function getPriceTierItemPrice($pt_id, CustomerVehicle $cv)
{
// price_tier is default
if ($pt_id == 0)
return null;
// find the item type for service offering
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'service_offering']);
if ($item_type == null)
return null;
// find the service offering
// check if customer vehicle has a motolite battery
// if yes, set the code to the motolite user service fee
if ($cv->hasMotoliteBattery())
$code = 'motolite_user_service_fee';
else
$code = 'fuel_service_fee';
$service = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
// check if service is null. If null, return null
if ($service == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $service->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getPriceTierFuelItemPrice($pt_id, $fuel_type)
{
// price_tier is default
if ($pt_id == 0)
return null;
// find the item type for service offering
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'service_offering']);
if ($item_type == null)
return null;
// find the service offering
$code = '';
if ($fuel_type == FuelType::GAS)
$code = 'fuel_gas_fee';
if ($fuel_type == FuelType::DIESEL)
$code = 'fuel_diesel_fee';
$service = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
// check if service is null. If null, return null
if ($service == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $service->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getTitle($fuel_type)
{
$title = '4L - ' . ucfirst($fuel_type);

View file

@ -8,16 +8,21 @@ use App\InvoiceRuleInterface;
use App\Entity\ServiceOffering;
use App\Entity\CustomerVehicle;
use App\Entity\ItemType;
use App\Ramcar\TransactionOrigin;
use App\Service\PriceTierManager;
class Jumpstart implements InvoiceRuleInterface
{
protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->pt_manager = $pt_manager;
}
public function getID()
@ -29,13 +34,21 @@ class Jumpstart implements InvoiceRuleInterface
{
$stype = $criteria->getServiceType();
$source = $criteria->getSource();
$pt_id = $criteria->getPriceTier();
$items = [];
if ($stype == $this->getID())
{
$cv = $criteria->getCustomerVehicle();
$fee = $this->getServiceTypeFee($source, $cv);
// check if price tier has item price
$pt_price = $this->getPriceTierItemPrice($pt_id, $source, $cv);
if ($pt_price == null)
$price = $this->getServiceTypeFee($source, $cv);
else
$price = $pt_price;
// add the service fee to items
$qty = 1;
@ -43,10 +56,10 @@ class Jumpstart implements InvoiceRuleInterface
'service_type' => $this->getID(),
'qty' => $qty,
'title' => $this->getServiceTitle(),
'price' => $fee,
'price' => $price,
];
$qty_price = bcmul($fee, $qty, 2);
$qty_price = bcmul($price, $qty, 2);
$total['total_price'] = bcadd($total['total_price'], $qty_price, 2);
}
@ -55,18 +68,8 @@ class Jumpstart implements InvoiceRuleInterface
public function getServiceTypeFee($source, CustomerVehicle $cv)
{
// check the source of JO
// (1) if from app, service fee is 0 if motolite user. jumpstart fee for app if non-motolite user.
// (2) any other source, jumpstart fees are charged whether motolite user or not
if ($source == TransactionOrigin::MOBILE_APP)
{
if ($cv->hasMotoliteBattery())
$code = 'motolite_user_service_fee';
else
$code = 'jumpstart_fee_mobile_app';
}
else
$code = 'jumpstart_fee';
// get the service fee code, depending on the JO source and if customer vehicle has a motolite battery
$code = $this->getServiceFeeCode($cv, $source);
$fee = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
@ -86,10 +89,68 @@ class Jumpstart implements InvoiceRuleInterface
return null;
}
protected function getPriceTierItemPrice($pt_id, $source, $cv)
{
// price_tier is default
if ($pt_id == 0)
return null;
// find the item type for service offering
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'service_offering']);
if ($item_type == null)
return null;
// get the service fee code, depending on the JO source and if customer vehicle has a motolite battery
$code = $this->getServiceFeeCode($cv, $source);
// find the service offering
$service = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
// check if service is null. If null, return null
if ($service == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $service->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getServiceTitle()
{
$title = 'Service - Troubleshooting fee';
return $title;
}
protected function getServiceFeeCode(CustomerVehicle $cv, $source)
{
// check the source of JO
// (1) if from app, service fee is 0 if motolite user. jumpstart fee for app if non-motolite user.
// (2) any other source, jumpstart fees are charged whether motolite user or not. Service fees for non-motolite
// and motolite users are now different (used to be the same)
if ($source == TransactionOrigin::MOBILE_APP)
{
if ($cv->hasMotoliteBattery())
$code = 'motolite_user_service_fee';
else
$code = 'jumpstart_fee_mobile_app';
}
else
{
error_log('hotline');
if ($cv->hasMotoliteBattery())
{
error_log('has motolite battery');
$code = 'motolite_user_jumpstart_fee';
}
else
$code = 'jumpstart_fee';
}
return $code;
}
}

View file

@ -7,14 +7,20 @@ use Doctrine\ORM\EntityManagerInterface;
use App\InvoiceRuleInterface;
use App\Entity\ServiceOffering;
use App\Entity\ItemType;
use App\Entity\CustomerVehicle;
use App\Service\PriceTierManager;
class JumpstartWarranty implements InvoiceRuleInterface
{
protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->pt_manager = $pt_manager;
}
public function getID()
@ -25,12 +31,21 @@ class JumpstartWarranty implements InvoiceRuleInterface
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$pt_id = $criteria->getPriceTier();
$items = [];
if ($stype == $this->getID())
{
$fee = $this->getServiceTypeFee();
$cv = $criteria->getCustomerVehicle();
// check if price tier has item price
$pt_price = $this->getPriceTierItemPrice($pt_id, $cv);
if ($pt_price == null)
$price = $this->getServiceTypeFee($cv);
else
$price = $pt_price;
// add the service fee to items
$qty = 1;
@ -38,18 +53,23 @@ class JumpstartWarranty implements InvoiceRuleInterface
'service_type' => $this->getID(),
'qty' => $qty,
'title' => $this->getServiceTitle(),
'price' => $fee,
'price' => $price,
];
$qty_price = bcmul($fee, $qty, 2);
$qty_price = bcmul($price, $qty, 2);
$total['total_price'] = bcadd($total['total_price'], $qty_price, 2);
}
return $items;
}
public function getServiceTypeFee()
public function getServiceTypeFee(CustomerVehicle $cv)
{
// check if user has motolite battery.
// Motolite users now have a service fee for jumpstart warranty
if ($cv->hasMotoliteBattery())
$code = 'motolite_user_jumpstart_warranty_fee';
else
$code = 'jumpstart_warranty_fee';
// find the service fee using the code
@ -72,6 +92,39 @@ class JumpstartWarranty implements InvoiceRuleInterface
return null;
}
protected function getPriceTierItemPrice($pt_id, CustomerVehicle $cv)
{
// price_tier is default
if ($pt_id == 0)
return null;
// find the item type for service offering
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'service_offering']);
if ($item_type == null)
return null;
// find the service offering
// check if user has motolite battery.
// Motolite users now have a service fee for jumpstart warranty
if ($cv->hasMotoliteBattery())
$code = 'motolite_user_jumpstart_warranty_fee';
else
$code = 'jumpstart_warranty_fee';
$service = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
// check if service is null. If null, return null
if ($service == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $service->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getServiceTitle()
{
$title = 'Service - Troubleshooting fee';

View file

@ -10,14 +10,19 @@ use App\Ramcar\ServiceType;
use App\Entity\ServiceOffering;
use App\Entity\CustomerVehicle;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class Overheat implements InvoiceRuleInterface
{
protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->pt_manager = $pt_manager;
}
public function getID()
@ -29,13 +34,22 @@ class Overheat implements InvoiceRuleInterface
{
$stype = $criteria->getServiceType();
$has_coolant = $criteria->hasCoolant();
$pt_id = $criteria->getPriceTier();
$items = [];
if ($stype == $this->getID())
{
$cv = $criteria->getCustomerVehicle();
$fee = $this->getServiceTypeFee($cv);
// check if price tier has item price
$pt_price = $this->getPriceTierItemPrice($pt_id, $cv);
if ($pt_price == null)
$price = $this->getServiceTypeFee($cv);
else
$price = $pt_price;
// add the service fee to items
$qty = 1;
@ -43,10 +57,10 @@ class Overheat implements InvoiceRuleInterface
'service_type' => $this->getID(),
'qty' => $qty,
'title' => $this->getServiceTitle(),
'price' => $fee,
'price' => $price,
];
$qty_fee = bcmul($qty, $fee, 2);
$qty_fee = bcmul($qty, $price, 2);
$total_price = $qty_fee;
if ($has_coolant)
@ -94,7 +108,7 @@ class Overheat implements InvoiceRuleInterface
// find the service fee using the code
// if we can't find the fee, return 0
$fee = $this->em->getRepository(ServiceOffering::class)->findOneBy($code);
$fee = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
if ($fee == null)
return 0;
@ -112,6 +126,39 @@ class Overheat implements InvoiceRuleInterface
return null;
}
protected function getPriceTierItemPrice($pt_id, CustomerVehicle $cv)
{
// price_tier is default
if ($pt_id == 0)
return null;
// find the item type for service offering
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'service_offering']);
if ($item_type == null)
return null;
// find the service offering
$code = 'overheat_fee';
// check if customer vehicle has a motolite battery
// if yes, set the code to the motolite user service fee
if ($cv->hasMotoliteBattery())
$code = 'motolite_user_service_fee';
$service = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
// check if service is null. If null, return null
if ($service == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $service->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getServiceTitle()
{
$title = 'Service - ' . ServiceType::getName(ServiceType::OVERHEAT_ASSISTANCE);

View file

@ -7,14 +7,19 @@ use Doctrine\ORM\EntityManagerInterface;
use App\InvoiceRuleInterface;
use App\Entity\ServiceOffering;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class PostRecharged implements InvoiceRuleInterface
{
protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->pt_manager = $pt_manager;
}
public function getID()
@ -25,22 +30,29 @@ class PostRecharged implements InvoiceRuleInterface
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$pt_id = $criteria->getPriceTier();
$items = [];
if ($stype == $this->getID())
{
$fee = $this->getServiceTypeFee();
// check if price tier has item price
$pt_price = $this->getPriceTierItemPrice($pt_id);
if ($pt_price == null)
$price = $this->getServiceTypeFee();
else
$price = $pt_price;
$qty = 1;
$items[] = [
'service_type' => $this->getID(),
'qty' => $qty,
'title' => $this->getServiceTitle(),
'price' => $fee,
'price' => $price,
];
$qty_price = bcmul($fee, $qty, 2);
$qty_price = bcmul($price, $qty, 2);
$total['total_price'] = bcadd($total['total_price'], $qty_price, 2);
}
@ -72,6 +84,33 @@ class PostRecharged implements InvoiceRuleInterface
return null;
}
protected function getPriceTierItemPrice($pt_id)
{
// price_tier is default
if ($pt_id == 0)
return null;
// find the item type for service offering
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'service_offering']);
if ($item_type == null)
return null;
// find the service offering
$code = 'post_recharged_fee';
$service = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
// check if service is null. If null, return null
if ($service == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $service->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getServiceTitle()
{
$title = 'Recharge fee';

View file

@ -7,14 +7,19 @@ use Doctrine\ORM\EntityManagerInterface;
use App\InvoiceRuleInterface;
use App\Entity\ServiceOffering;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class PostReplacement implements InvoiceRuleInterface
{
protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->pt_manager = $pt_manager;
}
public function getID()
@ -25,22 +30,29 @@ class PostReplacement implements InvoiceRuleInterface
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$pt_id = $criteria->getPriceTier();
$items = [];
if ($stype == $this->getID())
{
$fee = $this->getServiceTypeFee();
// check if price tier has item price
$pt_price = $this->getPriceTierItemPrice($pt_id);
if ($pt_price == null)
$price = $this->getServiceTypeFee();
else
$price = $pt_price;
$qty = 1;
$items[] = [
'service_type' => $this->getID(),
'qty' => $qty,
'title' => $this->getServiceTitle(),
'price' => $fee,
'price' => $price,
];
$qty_price = bcmul($fee, $qty, 2);
$qty_price = bcmul($price, $qty, 2);
$total['total_price'] = bcadd($total['total_price'], $qty_price, 2);
}
@ -71,6 +83,33 @@ class PostReplacement implements InvoiceRuleInterface
return null;
}
protected function getPriceTierItemPrice($pt_id)
{
// price_tier is default
if ($pt_id == 0)
return null;
// find the item type for service offering
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'service_offering']);
if ($item_type == null)
return null;
// find the service offering
$code = 'post_replacement_fee';
$service = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
// check if service is null. If null, return null
if ($service == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $service->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getServiceTitle()
{
$title = 'Battery replacement';

View file

@ -9,14 +9,19 @@ use App\InvoiceRuleInterface;
use App\Ramcar\ServiceType;
use App\Entity\ServiceOffering;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class Tax implements InvoiceRuleInterface
{
protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->pt_manager = $pt_manager;
}
public function getID()
@ -40,6 +45,7 @@ class Tax implements InvoiceRuleInterface
// compute tax per item if service type is battery sales
$stype = $criteria->getServiceType();
$pt = $criteria->getPriceTier();
if ($stype == ServiceType::BATTERY_REPLACEMENT_NEW)
{
@ -58,7 +64,13 @@ class Tax implements InvoiceRuleInterface
$battery = $entry['battery'];
$qty = $entry['qty'];
// check if price tier has item price for battery
$pt_price = $this->getPriceTierItemPrice($pt, $battery);
if ($pt_price == null)
$price = $battery->getSellingPrice();
else
$price = $pt_price;
$vat = $this->getTaxAmount($price, $tax_rate);
@ -96,6 +108,25 @@ class Tax implements InvoiceRuleInterface
return null;
}
protected function getPriceTierItemPrice($pt_id, $batt)
{
// price tier is default
if ($pt_id == 0)
return null;
// find the item type battery
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'battery']);
if ($item_type == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $batt->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getTaxAmount($price, $tax_rate)
{
$vat_ex_price = $this->getTaxExclusivePrice($price, $tax_rate);

View file

@ -8,14 +8,19 @@ use App\InvoiceRuleInterface;
use App\Entity\ServiceOffering;
use App\Entity\CustomerVehicle;
use App\Entity\ItemType;
use App\Service\PriceTierManager;
class TireRepair implements InvoiceRuleInterface
{
protected $em;
protected $pt_manager;
public function __construct(EntityManagerInterface $em)
public function __construct(EntityManagerInterface $em, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->pt_manager = $pt_manager;
}
public function getID()
@ -26,13 +31,22 @@ class TireRepair implements InvoiceRuleInterface
public function compute($criteria, &$total)
{
$stype = $criteria->getServiceType();
$has_sealant = $criteria->hasSealant();
$pt_id = $criteria->getPriceTier();
$items = [];
if ($stype == $this->getID())
{
$cv = $criteria->getCustomerVehicle();
$fee = $this->getServiceTypeFee($cv);
// check if price tier has item price
$pt_price = $this->getPriceTierItemPrice($pt_id, $cv);
if ($pt_price == null)
$price = $this->getServiceTypeFee($cv);
else
$price = $pt_price;
// add the service fee to items
$qty = 1;
@ -40,11 +54,31 @@ class TireRepair implements InvoiceRuleInterface
'service_type' => $this->getID(),
'qty' => $qty,
'title' => $this->getServiceTitle(),
'price' => $fee,
'price' => $price,
];
$qty_price = bcmul($fee, $qty, 2);
$total['total_price'] = bcadd($total['total_price'], $qty_price, 2);
$qty_fee = bcmul($qty, $price, 2);
$total_price = $qty_fee;
if ($has_sealant)
{
$sealant_fee_data = $this->getSealantFeeData();
$sealant_fee = $sealant_fee_data['fee'];
$sealant_title = $sealant_fee_data['title'];
$items[] = [
'service_type' => $this->getID(),
'qty' => $qty,
'title' => $sealant_title,
'price' => $sealant_fee,
];
$qty_price = bcmul($sealant_fee, $qty, 2);
$total_price = bcadd($total_price, $qty_price, 2);
}
$total['total_price'] = bcadd($total['total_price'], $total_price, 2);
}
return $items;
@ -79,10 +113,67 @@ class TireRepair implements InvoiceRuleInterface
return null;
}
protected function getPriceTierItemPrice($pt_id, CustomerVehicle $cv)
{
// price_tier is default
if ($pt_id == 0)
return null;
// find the item type for service offering
$item_type = $this->em->getRepository(ItemType::class)->findOneBy(['code' => 'service_offering']);
if ($item_type == null)
return null;
// find the service offering
$code = 'tire_repair_fee';
// check if customer vehicle has a motolite battery
// if yes, set the code to the motolite user service fee
if ($cv->hasMotoliteBattery())
$code = 'motolite_user_service_fee';
$service = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
// check if service is null. If null, return null
if ($service == null)
return null;
$item_type_id = $item_type->getID();
$item_id = $service->getID();
$price = $this->pt_manager->getItemPrice($pt_id, $item_type_id, $item_id);
return $price;
}
protected function getServiceTitle()
{
$title = 'Service - Flat Tire';
return $title;
}
public function getSealantFeeData()
{
$data = [
'fee' => 0.00,
'title' => '',
];
$code = 'tire_sealant_fee';
// find the service fee using the code
// if we can't find the fee, return 0
$fee = $this->em->getRepository(ServiceOffering::class)->findOneBy(['code' => $code]);
if ($fee != null)
{
$data = [
'fee' => $fee->getFee(),
'title' => $fee->getName(),
];
}
return $data;
}
}

View file

@ -2,12 +2,24 @@
namespace App\InvoiceRule;
use Doctrine\ORM\EntityManagerInterface;
use App\InvoiceRuleInterface;
use App\Ramcar\TradeInType;
use App\Ramcar\ServiceType;
use App\Entity\BatterySize;
class TradeIn implements InvoiceRuleInterface
{
protected $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function getID()
{
return 'trade-in';
@ -21,7 +33,6 @@ class TradeIn implements InvoiceRuleInterface
$entries = $criteria->getEntries();
foreach($entries as $entry)
{
$batt = $entry['battery'];
$qty = $entry['qty'];
$trade_in_type = null;
@ -30,7 +41,9 @@ class TradeIn implements InvoiceRuleInterface
if ($trade_in_type != null)
{
$ti_rate = $this->getTradeInRate($batt, $trade_in_type);
$batt_size = $entry['battery_size'];
$ti_rate = $this->getTradeInRate($batt_size, $trade_in_type);
$qty_ti = bcmul($ti_rate, $qty, 2);
@ -40,8 +53,10 @@ class TradeIn implements InvoiceRuleInterface
$price = bcmul($ti_rate, -1, 2);
$items[] = [
'battery_size' => $batt_size,
'trade_in_type' => $trade_in_type,
'qty' => $qty,
'title' => $this->getTitle($batt, $trade_in_type),
'title' => $this->getTitle($batt_size, $trade_in_type),
'price' => $price,
];
}
@ -57,13 +72,47 @@ class TradeIn implements InvoiceRuleInterface
public function validateInvoiceItems($criteria, $invoice_items)
{
// check service type. Only battery sales and battery warranty should have invoice items.
$stype = $criteria->getServiceType();
if ($stype != ServiceType::BATTERY_REPLACEMENT_NEW)
return null;
// return error if there's a problem, false otherwise
if (!empty($invoice_items))
{
// check if this is a valid battery
foreach ($invoice_items as $item)
{
if (isset($item['battery_size']))
{
$battery_size = $this->em->getRepository(BatterySize::class)->find($item['battery_size']);
if (empty($battery_size))
{
$error = 'Invalid battery size specified.';
return $error;
}
// quantity
$qty = $item['quantity'];
if ($qty < 1)
continue;
// check if trade in is set and if trade in type if valid
if (!empty($item['trade_in']) && TradeInType::validate($item['trade_in']))
{
$trade_in = $item['trade_in'];
$criteria->addTradeInEntry($battery_size, $trade_in, $qty);
}
}
}
}
return null;
}
protected function getTradeInRate($battery, $trade_in_type)
protected function getTradeInRate($size, $trade_in_type)
{
$size = $battery->getSize();
switch ($trade_in_type)
{
case TradeInType::MOTOLITE:
@ -77,9 +126,9 @@ class TradeIn implements InvoiceRuleInterface
return 0;
}
protected function getTitle($battery, $trade_in_type)
protected function getTitle($battery_size, $trade_in_type)
{
$title = 'Trade-in ' . TradeInType::getName($trade_in_type) . ' ' . $battery->getSize()->getName() . ' battery';
$title = 'Trade-in ' . TradeInType::getName($trade_in_type) . ' ' . $battery_size->getName() . ' battery';
return $title;
}

View file

@ -23,8 +23,8 @@ class DeliveryStatus extends NameValue
const COLLECTION = [
'rider_assign' => 'Assigned Rider',
'requeue' => 'Requeue',
'accept' => 'Rider Accept',
'arrive' => 'Rider Arrive',
'rider_accept' => 'Rider Accept',
'rider_arrive' => 'Rider Arrive',
'rider_edit' => 'Rider Edit',
'rider_depart_hub' => 'Rider Depart Hub',
'rider_arrive_hub_pre_jo' => 'Rider Arrive Hub Pre JO',

View file

@ -12,6 +12,7 @@ class HubCriteria
protected $limit_results; // number of results to return
protected $limit_distance; // distance limit for search in km
protected $flag_inventory_check; // flag if we need to check for inventory
protected $flag_riders_check; // flag if we need to check for riders available
protected $jo_type; // jo service needed
protected $date_time; // date and time to check if hub is open or not
protected $items; // array of items: items[sku] = quantity to check for
@ -20,6 +21,10 @@ class HubCriteria
protected $flag_round_robin; // flag if we use round robin or not
protected $jo_id; // JO id. This is null if called from mobile API
protected $customer_id; // customer id
protected $customer_class; // customer class
protected $order_date; // date JO was created
protected $service_type; // service type of JO
protected $jo_origin; // origin of JO
public function __construct()
{
@ -29,12 +34,17 @@ class HubCriteria
$this->jo_type = '';
$this->date_time = new DateTime();
$this->flag_inventory_check = false;
$this->flag_riders_check = false;
$this->items = [];
$this->payment_method = '';
$flag_emergency = false;
$flag_round_robin = false;
$jo_id = null;
$customer_id = null;
$this->flag_emergency = false;
$this->flag_round_robin = false;
$this->jo_id = null;
$this->customer_id = null;
$this->customer_class = null;
$this->order_date = new DateTime();
$this->service_type = null;
$this->jo_origin = null;
}
public function setPoint(Point $point)
@ -81,6 +91,17 @@ class HubCriteria
return $this->flag_inventory_check;
}
public function setRidersCheck($flag_riders_check = true)
{
$this->flag_riders_check = $flag_riders_check;
return $this;
}
public function hasRidersCheck()
{
return $this->flag_riders_check;
}
public function setJoType($jo_type)
{
// TODO: validate the jo type
@ -171,5 +192,48 @@ class HubCriteria
return $this->customer_id;
}
public function setCustomerClass($customer_class)
{
$this->customer_class = $customer_class;
return $this;
}
public function getCustomerClass()
{
return $this->customer_class;
}
public function setOrderDate($order_date)
{
$this->order_date = $order_date;
return $this;
}
public function getOrderDate()
{
return $this->order_date;
}
public function setServiceType($service_type)
{
$this->service_type = $service_type;
return $this;
}
public function getServiceType()
{
return $this->service_type;
}
public function setJoOrigin($jo_origin)
{
$this->jo_origin = $jo_origin;
return $this;
}
public function getJoOrigin()
{
return $this->jo_origin;
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Ramcar;
class InsuranceBodyType extends NameValue
{
const SEDAN = 'sedan';
const SUV = 'suv';
const TRUCK = 'truck';
const MOTORCYCLE = 'motorcycle';
const COLLECTION = [
'SEDAN' => 'Sedan',
'SUV' => 'SUV',
'TRUCK' => 'Truck',
'MOTORCYCLE' => 'Motorcycle',
];
}

View file

@ -17,6 +17,8 @@ class InvoiceCriteria
protected $service_charges;
protected $flag_taxable;
protected $source; // use Ramcar's TransactionOrigin
protected $price_tier;
protected $flag_sealant;
// entries are battery and trade-in combos
protected $entries;
@ -32,6 +34,8 @@ class InvoiceCriteria
$this->service_charges = [];
$this->flag_taxable = false;
$this->source = '';
$this->price_tier = 0; // set to default
$this->flag_sealant = false;
}
public function setServiceType($stype)
@ -108,6 +112,17 @@ class InvoiceCriteria
$this->entries[] = $entry;
}
public function addTradeInEntry($battery_size, $trade_in, $qty)
{
$entry = [
'battery_size' => $battery_size,
'trade_in' => $trade_in,
'qty' => $qty
];
$this->entries[] = $entry;
}
public function getEntries()
{
return $this->entries;
@ -179,4 +194,25 @@ class InvoiceCriteria
return $this->source;
}
public function setPriceTier($price_tier)
{
$this->price_tier = $price_tier;
return $this;
}
public function getPriceTier()
{
return $this->price_tier;
}
public function setHasSealant($flag = true)
{
$this->flag_sealant = $flag;
return $this;
}
public function hasSealant()
{
return $this->flag_sealant;
}
}

View file

@ -15,6 +15,13 @@ class JORejectionReason extends NameValue
const STORE_CLOSED = 'store_closed';
const NO_CREDIT_CARD = 'no_credit_card';
const DISCOUNT = 'discount';
const STORE_CLOSED_SCHEDULED = 'store_closed_scheduled';
const STORE_CLOSED_HALF_DAY = 'store_closed_half_day';
const STORE_CLOSED_NO_ADVISE = 'store_closed_no_advise';
const PRIORITY_HUB_WTY_CLAIM = 'priority_hub_wty_claim';
const PRIORITY_HUB_JUMPSTART = 'priority_hub_jumpstart';
const PRIORITY_HUB_RESQ_REQUEST = 'priority_hub_resq_req';
const CUSTOMER_REQUEST = 'customer_request';
const COLLECTION = [
'administrative' => 'ADMINISTRATIVE',
@ -28,5 +35,16 @@ class JORejectionReason extends NameValue
'store_closed' => 'STORE CLOSED',
'no_credit_card' => 'NO CREDIT CARD PAYMENT / NO TERMINAL',
'discount' => 'DISCOUNT',
'store_closed_scheduled' => 'STORE CLOSED (ON SCHEDULE)',
'store_closed_half_day' => 'STORE CLOSED (HALF DAY)',
'store_closed_no_advise' => 'STORE CLOSED (NO ADVISE)',
'priority_hub_wty_claim' => 'PRIORITY HUB OUTLET FOR WARRANTY CLAIM',
'priority_hub_jumpstart' => 'PRIORITY HUB OUTLET FOR JUMPSTART',
'priority_hub_resq_req' => 'PRIORITY HUB OUTLET FOR RESQ-Q REQUEST',
'customer_request' => 'CUSTOMER REQUEST',
];
const BLACKLIST = [
self::ADMINISTRATIVE => true,
];
}

View file

@ -4,9 +4,21 @@ namespace App\Ramcar;
class NameValue
{
const BLACKLIST = [];
static public function getCollection()
{
return static::COLLECTION;
$result = [];
$blacklist = static::getBlacklist();
// filter from blacklist
foreach(static::COLLECTION as $key => $row) {
if (!isset($blacklist[$key])) {
$result[$key] = $row;
}
}
return $result;
}
static public function validate($value)
@ -24,4 +36,9 @@ class NameValue
return 'Unknown';
}
static public function getBlacklist()
{
return static::BLACKLIST ?? [];
}
}

View file

@ -81,6 +81,7 @@ class HubDistributor
'distance' => $hub_data['distance'],
'duration' => $hub_data['duration'],
'jo_count' => $hub_jo_count,
'inventory' => $hub_data['inventory'] ?? 0,
];
}
else
@ -91,6 +92,7 @@ class HubDistributor
'distance' => $hub_data['distance'],
'duration' => $hub_data['duration'],
'jo_count' => 0,
'inventory' => $hub_data['inventory'] ?? 0,
];
}
}

View file

@ -0,0 +1,144 @@
<?php
namespace App\Service\HubFilter;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use App\Service\HubFilterLogger;
use App\Entity\Hub;
use App\Entity\JobOrder;
use App\Entity\JORejection;
use App\Ramcar\ServiceType;
use App\Ramcar\JORejectionReason;
use App\Service\RisingTideGateway;
use DateTime;
class BaseHubFilter
{
protected $id;
protected $jo_id;
protected $customer_id;
protected $hub_filter_logger;
protected $em;
protected $rt;
protected $trans;
public function __construct(HubFilterLogger $hub_filter_logger, EntityManagerInterface $em, RisingTideGateway $rt, TranslatorInterface $trans)
{
$this->hub_filter_logger = $hub_filter_logger;
$this->em = $em;
$this->rt = $rt;
$this->trans = $trans;
error_log("-------------------");
error_log("HUB FILTER RUNNING: " . $this->getID());
error_log("-------------------");
}
public function getID(): string
{
return $this->id;
}
public function setJOID(int $jo_id)
{
$this->jo_id = $jo_id;
return $this;
}
public function getJOID(): int
{
return $this->jo_id;
}
public function setCustomerID(int $customer_id)
{
$this->customer_id = $customer_id;
return $this;
}
public function getCustomerID(): int
{
return $this->customer_id;
}
public function log(Hub $hub): void
{
$this->hub_filter_logger->logFilteredHub($hub, $this->getID(), $this->getJOID(), $this->getCustomerID());
// log to file
$filename = '/../../../var/log/hub_rejection.log';
$date = date("Y-m-d H:i:s");
// build log entry
$entry = implode("", [
"[JO: " . $this->getJOID() . "]",
"[" . $date . "]",
"[" . $this->getID() . "]",
" " . $hub->getName() . " (ID: " . $hub->getID() . ")",
"\r\n",
]);
@file_put_contents(__DIR__ . $filename, $entry, FILE_APPEND);
}
protected function createRejectionEntry($hub, $reason, $remarks = ""): JORejection
{
$jo = $this->em->getRepository(JobOrder::class)->find($this->getJOID());
$robj = new JORejection();
$robj->setDateCreate(new DateTime())
->setHub($hub)
->setJobOrder($jo)
->setReason($reason)
->setRemarks(implode(" ", ["Automatically filtered by hub selector.", $remarks]));
$this->em->persist($robj);
$this->em->flush();
return $robj;
}
protected function sendSMSMessage($hub, $order_date, $service_type, $rejection, $reason = "", $remarks = ""): void
{
$jo_id = $this->getJOID();
// check if we already have a rejection record for this hub and JO. this also means an SMS was already sent
$rejection_count = $this->em->createQueryBuilder()
->select('count(r)')
->from(JORejection::class, 'r')
->where('r.job_order = :jo_id')
->andWhere('r.hub = :hub_id')
->andWhere('r.id != :rejection_id')
->setParameter('jo_id', $jo_id)
->setParameter('hub_id', $hub->getID())
->setParameter('rejection_id', $rejection->getID())
->getQuery()
->getSingleScalarResult();
// if we already have a rejection record for this hub and JO, do not send another SMS
if ($rejection_count >= 1) {
error_log("ALREADY SENT REJECTION SMS TO HUB " . $hub->getID() . " FOR JO " . $jo_id);
return;
}
$message = 'Job Order #: ' . $jo_id . "\n" .
'Order Date and Time: ' . $order_date->format('d M Y g:i A') . "\n" .
'Date and Time Rejected: ' . $rejection->getDateCreate()->format('d M Y g:i A') . "\n" .
'Enrollee Name: ' . implode(" - ", [$hub->getName(), $hub->getBranch()]) . "\n" .
'Reason of Rejection: ' . $reason . "\n" .
'Remarks: ' . $remarks . "\n" .
'Type of Service: ' . ServiceType::getName($service_type);
error_log("SENDING SMS MESSAGE:\r\n" . $message);
// send SMS message to hub
$this->rt->sendSMS(
$hub->getNotifNumber(),
$this->trans->trans('message.battery_brand_allcaps'),
$message
);
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace App\Service\HubFilter\Filters;
use App\Service\HubFilter\BaseHubFilter;
use App\Service\HubFilter\HubFilterInterface;
class DateAndTimeHubFilter extends BaseHubFilter implements HubFilterInterface
{
protected $id = 'date_and_time';
public function getRequestedParams() : array
{
return [
'date_time',
];
}
public function filter(array $hubs, array $params = []) : array
{
if ($params['date_time'] == null)
return $hubs;
$results = [];
foreach ($hubs as $hub_data)
{
// go through each hub's opening times to check if hub is open
// for the specified time
// get hub opening and closing times
// TODO: maybe in the future, might also have to check if hub
// is open/available on date/day
$hub = $hub_data['hub'];
$time_open = $hub->getTimeOpen()->format("H:i:s");
$time_close = $hub->getTimeClose()->format("H:i:s");
$filter_time = $params['date_time']->format("H:i:s");
if (($filter_time >= $time_open) &&
($filter_time <= $time_close))
{
$results[] = [
'hub' => $hub,
'db_distance' => $hub_data['db_distance'],
'distance' => $hub_data['distance'],
'duration' => $hub_data['duration'],
'jo_count' => 0,
'inventory' => $hub_data['inventory'],
];
}
else
$this->log($hub);
}
return $results;
}
}

View file

@ -0,0 +1,164 @@
<?php
namespace App\Service\HubFilter\Filters;
use App\Service\HubFilter\BaseHubFilter;
use App\Service\HubFilter\HubFilterInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use App\Ramcar\JORejectionReason;
use App\Ramcar\ServiceType;
use App\Ramcar\CustomerClassification;
use App\Service\InventoryManager;
use App\Service\HubFilterLogger;
use App\Service\RisingTideGateway;
use App\Entity\Battery;
use App\Ramcar\TransactionOrigin;
class InventoryHubFilter extends BaseHubFilter implements HubFilterInterface
{
protected $id = 'no_inventory';
protected $im;
public function __construct(HubFilterLogger $hub_filter_logger, EntityManagerInterface $em, RisingTideGateway $rt, TranslatorInterface $trans, InventoryManager $im)
{
parent::__construct($hub_filter_logger, $em, $rt, $trans);
$this->im = $im;
}
public function getRequestedParams() : array
{
return [
'flag_inventory_check',
'customer_class',
'jo_type',
'jo_origin',
'order_date',
'service_type',
'items',
];
}
public function filter(array $hubs, array $params = []) : array
{
// check if this is enabled
if (!$params['flag_inventory_check']) {
error_log("INVENTORY CHECK " . $this->getJOID() . ": DISABLED");
return $hubs;
}
// check customer class
if ((!empty($params['customer_class']) && $params['customer_class'] == CustomerClassification::VIP) ||
$params['jo_origin'] === TransactionOrigin::VIP) {
error_log("INVENTORY CHECK " . $this->getJOID() . ": VIP CLASS");
return $hubs;
}
// check item list is not empty
if (empty($params['items'])) {
error_log("INVENTORY CHECK " . $this->getJOID() . ": NO ITEMS");
return $hubs;
}
// check this is a battery item related JO
if ($params['jo_type'] != ServiceType::BATTERY_REPLACEMENT_NEW &&
$params['jo_type'] != ServiceType::BATTERY_REPLACEMENT_WARRANTY
) {
error_log("INVENTORY CHECK " . $this->getJOID() . ": INVALID SERVICE TYPE: " . $params['jo_type']);
return $hubs;
}
// get a list of all hubs with branch codes
$branch_codes = [];
foreach ($hubs as $hub_data) {
$branch_code = $hub_data['hub']->getBranchCode();
if (!empty($branch_code)) {
$branch_codes[] = $branch_code;
}
};
$hubs_to_filter = [];
$results = [];
$qtys = [];
// call inventory manager for all hubs for selected SKUs
$skus = array_keys($params['items']);
error_log("CHECKING INVENTORY FOR " . count($skus) . " ITEM(S) ON HUBS " . count($branch_codes) . "...");
$branches = $this->im->getBranchesInventory($branch_codes, $skus);
error_log("REQUEST COMPLETE, RESULT COUNT: " . count($branches));
// check each result to see if sufficient quantity exists to meet request
foreach ($branches as $branch) {
if (isset($branch['BranchCode'])) {
// filter out branch if it does not have sufficient inventory
if (!isset($params['items'][$branch['SapCode']]) || $branch['Quantity'] < $params['items'][$branch['SapCode']] &&
!isset($hubs_to_filter[$branch['BranchCode']])
) {
error_log("FILTERING BRANCH WITH NO INVENTORY: " . $branch['BranchCode']);
$hubs_to_filter[$branch['BranchCode']] = true;
} else {
// save inventory count so we don't have to recheck later
$qtys[$branch['BranchCode']] = $branch['Quantity'];
}
}
}
// get battery models for each requested SKU
$batteries = [];
foreach ($skus as $sku) {
$bobj = $this->em->getRepository(Battery::class)->findOneBy(['sap_code' => $sku]);
$batteries[] = implode(" ", [$bobj->getModel()->getName(), $bobj->getSize()->getName()]);
}
$battery_string = implode(", ", $batteries);
// remove filtered hubs from list
foreach ($hubs as $hub_data) {
$hub = $hub_data['hub'];
$branch_code = $hub_data['hub']->getBranchCode();
// check if we are filtering this hub
if (isset($hubs_to_filter[$branch_code]) || empty($branch_code) || !isset($qtys[$branch_code])) {
// if we have a JO, create rejection record and notify
$jo_id = $this->getJOID();
if (!empty($jo_id)) {
// create rejection report entry
$robj = $this->createRejectionEntry(
$hub,
JORejectionReason::NO_STOCK_SALES,
"SKU(s): " . $battery_string,
);
// build SMS message
$this->sendSMSMessage(
$hub,
$params['order_date'],
$params['service_type'],
$robj,
JORejectionReason::getName(JORejectionReason::NO_STOCK_SALES),
"Requested SKU(s) - " . $battery_string
);
}
// log this filter
$this->log($hub);
error_log("FILTERED HUB " . $hub->getID() . " (no_inventory)");
} else {
// include inventory in hub data
$hub_data['inventory'] = $qtys[$branch_code];
// we only include branches with branch codes and quantities
$results[] = $hub_data;
}
}
// return filtered hubs
return $results;
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace App\Service\HubFilter\Filters;
use App\Service\HubFilter\BaseHubFilter;
use App\Service\HubFilter\HubFilterInterface;
class JoTypeHubFilter extends BaseHubFilter implements HubFilterInterface
{
protected $id = 'job_order_type';
public function getRequestedParams() : array
{
return [
'flag_emergency',
'jo_type',
];
}
public function filter(array $hubs, array $params = []) : array
{
if ($params['flag_emergency'])
return $hubs;
if (empty($params['jo_type']))
return $hubs;
$results = [];
foreach ($hubs as $hub_data)
{
$hub = $hub_data['hub'];
// TODO: for now, have this return true
$has_jo_type = true;
// check if hub offers the jo_type
// TODO: add service to hub
if ($has_jo_type)
$results[] = [
'hub' => $hub,
'db_distance' => $hub_data['db_distance'],
'distance' => $hub_data['distance'],
'duration' => $hub_data['duration'],
'jo_count' => 0,
'inventory' => $hub_data['inventory'],
];
else
$this->log($hub);
}
return $results;
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace App\Service\HubFilter\Filters;
use App\Service\HubFilter\BaseHubFilter;
use App\Service\HubFilter\HubFilterInterface;
class MaxResultsHubFilter extends BaseHubFilter implements HubFilterInterface
{
protected $id = 'max_results';
public function getRequestedParams() : array
{
return [
'limit_results',
];
}
public function filter(array $hubs, array $params = []) : array
{
if (empty($params['limit_results']))
return $hubs;
$results = [];
for ($i = 0; $i < count($hubs); $i++)
{
if ($i < $params['limit_results'])
$results[] = $hubs[$i];
else
$this->log($hubs[$i]['hub']);
}
return $results;
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace App\Service\HubFilter\Filters;
use App\Service\HubFilter\BaseHubFilter;
use App\Service\HubFilter\HubFilterInterface;
class PaymentMethodHubFilter extends BaseHubFilter implements HubFilterInterface
{
protected $id = 'no_payment_method';
public function getRequestedParams() : array
{
return [
'flag_emergency',
'payment_method',
];
}
public function filter(array $hubs, array $params = []) : array
{
if ($params['flag_emergency'])
return $hubs;
if (empty($params['payment_method']))
return $hubs;
$results = [];
foreach ($hubs as $hub_data)
{
$hub = $hub_data['hub'];
// name of payment method is what is saved
$payment_methods = $hub->getPaymentMethods();
if ($payment_methods != null)
{
$flag_found_pmethod = false;
foreach ($payment_methods as $pmethod)
{
if ($pmethod == $params['payment_method'])
{
$results[] = [
'hub' => $hub,
'db_distance' => $hub_data['db_distance'],
'distance' => $hub_data['distance'],
'duration' => $hub_data['duration'],
'jo_count' => 0,
'inventory' => $hub_data['inventory'],
];
}
$flag_found_pmethod = true;
}
if (!$flag_found_pmethod)
$this->log($hub);
}
else
$this->log($hub);
}
return $results;
}
}

View file

@ -0,0 +1,75 @@
<?php
namespace App\Service\HubFilter\Filters;
use App\Service\HubFilter\BaseHubFilter;
use App\Service\HubFilter\HubFilterInterface;
use App\Ramcar\JORejectionReason;
use App\Ramcar\CustomerClassification;
class RiderAvailabilityHubFilter extends BaseHubFilter implements HubFilterInterface
{
protected $id = 'no_available_rider';
public function getRequestedParams() : array
{
return [
'flag_riders_check',
'customer_class',
'order_date',
'service_type',
];
}
public function filter(array $hubs, array $params = []) : array
{
// check if this is enabled
if (!$params['flag_riders_check']) {
return $hubs;
}
// check customer class
if (!empty($params['customer_class']) && $params['customer_class'] == CustomerClassification::VIP) {
error_log("RIDER CHECK " . $this->getJOID() . ": VIP CLASS");
return $hubs;
}
$results = [];
foreach ($hubs as $hub_data) {
$hub = $hub_data['hub'];
$available_riders = count($hub->getAvailableRiders());
// check we have available riders
error_log("TOTAL RIDERS: " . $available_riders);
if ($available_riders === 0) {
// if we have a JO, create rejection record and notify
$jo_id = $this->getJOID();
if (!empty($jo_id)) {
// create rejection report entry
$robj = $this->createRejectionEntry($hub, JORejectionReason::NO_RIDER_AVAILABLE);
// build SMS message
$this->sendSMSMessage(
$hub,
$params['order_date'],
$params['service_type'],
$robj,
JORejectionReason::getName(JORejectionReason::NO_RIDER_AVAILABLE),
);
}
// log this filter
$this->log($hub);
error_log("FILTERED HUB " . $hub->getID() . " (no_available_rider)");
} else {
$results[] = $hub_data;
}
}
return $results;
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace App\Service\HubFilter\Filters;
use App\Service\HubFilter\BaseHubFilter;
use App\Service\HubFilter\HubFilterInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use App\Service\HubDistributor;
use App\Service\HubFilterLogger;
use App\Service\RisingTideGateway;
class RoundRobinHubFilter extends BaseHubFilter implements HubFilterInterface
{
protected $id = 'round_robin';
protected $hub_distributor;
public function __construct(HubFilterLogger $hub_filter_logger, EntityManagerInterface $em, RisingTideGateway $rt, TranslatorInterface $trans, HubDistributor $hub_distributor)
{
parent::__construct($hub_filter_logger, $em, $rt, $trans);
$this->hub_distributor = $hub_distributor;
}
public function getRequestedParams() : array
{
return [
'flag_round_robin',
];
}
public function filter(array $hubs, array $params = []) : array
{
if (!$params['flag_round_robin'])
return $hubs;
$results = [];
// call hub distributor service
$arranged_hubs = $this->hub_distributor->arrangeHubs($hubs);
$results = $arranged_hubs;
return $results;
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Service\HubFilter;
interface HubFilterInterface
{
public function getID() : string;
public function filter(array $hubs, array $params = []) : array;
public function setJOID(int $jo_id);
public function getJOID() : int;
public function setCustomerID(int $customer_id);
public function getCustomerID() : int;
public function getRequestedParams() : array;
}

View file

@ -5,21 +5,20 @@ namespace App\Service;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use CrEOF\Spatial\PHP\Types\Geometry\Point;
use App\Entity\Hub;
use App\Service\HubDistributor;
use App\Service\InventoryManager;
use App\Service\HubFilterLogger;
use App\Service\RisingTideGateway;
use App\Ramcar\HubCriteria;
use App\Ramcar\ServiceType;
class HubSelector
{
protected $container;
protected $em;
protected $im;
protected $hub_distributor;
@ -27,10 +26,11 @@ class HubSelector
protected $trans;
protected $rt;
public function __construct(EntityManagerInterface $em, InventoryManager $im,
public function __construct(ContainerInterface $container, EntityManagerInterface $em, InventoryManager $im,
HubDistributor $hub_distributor, HubFilterLogger $hub_filter_logger,
TranslatorInterface $trans, RisingTideGateway $rt)
{
$this->container = $container;
$this->em = $em;
$this->im = $im;
$this->hub_distributor = $hub_distributor;
@ -39,6 +39,18 @@ class HubSelector
$this->rt = $rt;
}
protected function getActiveFilters(): array
{
$fnames = explode(",", $this->container->getParameter('enabled_hub_filters'));
$enabled_filters = [];
foreach ($fnames as $filter) {
$enabled_filters[] = 'App\\Service\\HubFilter\\Filters\\' . $filter;
}
return $enabled_filters;
}
public function find(HubCriteria $criteria)
{
$point = $criteria->getPoint();
@ -46,15 +58,20 @@ class HubSelector
$limit_distance = $criteria->getLimitDistance();
$jo_type = $criteria->getJoType();
$flag_inventory_check = $criteria->hasInventoryCheck();
$flag_riders_check = $criteria->hasRidersCheck();
$items = $criteria->getItems();
$date_time = $criteria->getDateTime();
$payment_method = $criteria->getPaymentMethod();
$flag_emergency = $criteria->isEmergency();
$flag_round_robin = $criteria->isRoundRobin();
$jo_id = $criteria->getJobOrderId();
$jo_origin = $criteria->getJoOrigin();
$customer_id = $criteria->getCustomerId();
$customer_class = $criteria->getCustomerClass();
$results = [];
// needed for JORejection records and SMS notifs
$order_date = $criteria->getOrderDate();
$service_type = $criteria->getServiceType();
// error_log('payment methods ' . $payment_method);
// error_log('distance limit ' . $limit_distance);
@ -63,278 +80,52 @@ class HubSelector
// get all the hubs within distance
$filtered_hubs = $this->getClosestHubs($point, $limit_distance, $jo_id, $customer_id);
// error_log('closest hubs ' . json_encode($filtered_hubs));
// filter the first hub results for date and opening times
$hubs_date_time = $this->filterHubsByDateAndTime($filtered_hubs, $date_time, $jo_id, $customer_id);
$filtered_hubs = $hubs_date_time;
// error_log('date_time hubs ' . json_encode($filtered_hubs));
if (!$flag_emergency)
{
// filter jo types
$hubs_jo_type = $this->filterHubsByJoType($filtered_hubs, $jo_type, $jo_id, $customer_id);
$filtered_hubs = $hubs_jo_type;
//error_log('jo_type hubs ' . json_encode($filtered_hubs));
// filter hubs by payment methods
$hubs_payment_method = $this->filterHubsByPaymentMethod($filtered_hubs, $payment_method, $jo_id, $customer_id);
$filtered_hubs = $hubs_payment_method;
//error_log('payment hubs ' . json_encode($filtered_hubs));
// inventory filter
$hubs_inventory = $this->filterHubsByInventory($filtered_hubs, $flag_inventory_check,
$jo_type, $items, $jo_id, $customer_id);
$filtered_hubs = $hubs_inventory;
//error_log('inventory hubs ' . json_encode($filtered_hubs));
// round robin filter
$hubs_round_robin = $this->filterHubsByRoundRobin($filtered_hubs, $flag_round_robin);
$filtered_hubs = $hubs_round_robin;
// error_log('round robin hubs ' . json_encode($filtered_hubs));
// max results filter
$hubs_max_result = $this->filterHubsByMaxResults($filtered_hubs, $limit_results, $jo_id, $customer_id);
$filtered_hubs = $hubs_max_result;
}
$results = $filtered_hubs;
// error_log(json_encode($results));
return $results;
}
protected function filterHubsByRoundRobin($hubs, $flag_round_robin)
{
if (empty($hubs))
return $hubs;
if (!$flag_round_robin)
return $hubs;
$results = [];
// call hub distributor service
$arranged_hubs = $this->hub_distributor->arrangeHubs($hubs);
$results = $arranged_hubs;
return $results;
}
protected function filterHubsByMaxResults($hubs, $limit_result, $jo_id, $customer_id)
{
if (empty($hubs))
return $hubs;
if (empty($limit_result))
return $hubs;
$results = [];
for ($i = 0; $i < count($hubs); $i++)
{
if ($i < $limit_result)
$results[] = $hubs[$i];
else
$this->hub_filter_logger->logFilteredHub($hubs[$i]['hub'], 'max_results', $jo_id, $customer_id);
}
return $results;
}
protected function filterHubsByJoType($hubs, $jo_type, $jo_id, $customer_id)
{
if (empty($hubs))
return $hubs;
if (empty($jo_type))
return $hubs;
$results = [];
foreach ($hubs as $hub_data)
{
$hub = $hub_data['hub'];
// TODO: for now, have this return true
$has_jo_type = true;
// check if hub offers the jo_type
// TODO: add service to hub
if ($has_jo_type)
$results[] = [
'hub' => $hub,
'db_distance' => $hub_data['db_distance'],
'distance' => $hub_data['distance'],
'duration' => $hub_data['duration'],
'jo_count' => 0,
// build param list
$params = [
'date_time' => $date_time,
'flag_inventory_check' => $flag_inventory_check,
'customer_class' => $customer_class,
'jo_type' => $jo_type,
'jo_origin' => $jo_origin,
'order_date' => $order_date,
'service_type' => $service_type,
'items' => $items,
'flag_emergency' => $flag_emergency,
'limit_results' => $limit_results,
'payment_method' => $payment_method,
'flag_riders_check' => $flag_riders_check,
'flag_round_robin' => $flag_round_robin,
];
else
$this->hub_filter_logger->logFilteredHub($hub, 'job_order_type', $jo_id, $customer_id);
// loop through all enabled filters
foreach ($this->getActiveFilters() as $hub_filter) {
// no hubs left to filter
if (empty($filtered_hubs)) {
break;
}
return $results;
$f = $this->container->get($hub_filter);
// check if supported area is exempted from this filter
if ($this->isExemptedByArea($f->getID(), $point)) {
continue;
}
protected function filterHubsByPaymentMethod($hubs, $payment_method, $jo_id, $customer_id)
{
if (empty($hubs))
return $hubs;
if (empty($payment_method))
return $hubs;
$f->setJOID($jo_id);
$f->setCustomerID($customer_id);
$results = [];
foreach ($hubs as $hub_data)
{
$hub = $hub_data['hub'];
// get requested params only
$req_params = array_intersect_key($params, array_flip($f->getRequestedParams()));
// name of payment method is what is saved
$payment_methods = $hub->getPaymentMethods();
if ($payment_methods != null)
{
$flag_found_pmethod = false;
foreach ($payment_methods as $pmethod)
{
if ($pmethod == $payment_method)
{
$results[] = [
'hub' => $hub,
'db_distance' => $hub_data['db_distance'],
'distance' => $hub_data['distance'],
'duration' => $hub_data['duration'],
'jo_count' => 0,
];
}
$flag_found_pmethod = true;
// filter hub list
$filtered_hubs = $f->filter($filtered_hubs, $req_params);
// error_log($f->getID() . ' hubs ' . json_encode($filtered_hubs));
}
if (!$flag_found_pmethod)
$this->hub_filter_logger->logFilteredHub($hub, 'no_payment_method', $jo_id, $customer_id);
}
else
$this->hub_filter_logger->logFilteredHub($hub, 'no_payment_method', $jo_id, $customer_id);
}
// error_log('final hub list ' . json_encode($filtered_hubs));
return $results;
}
protected function filterHubsByDateAndTime($hubs, $date_time, $jo_id, $customer_id)
{
if (empty($hubs))
return $hubs;
if ($date_time == null)
return $hubs;
$results = [];
foreach ($hubs as $hub_data)
{
// go through each hub's opening times to check if hub is open
// for the specified time
// get hub opening and closing times
// TODO: maybe in the future, might also have to check if hub
// is open/available on date/day
$hub = $hub_data['hub'];
$time_open = $hub->getTimeOpen()->format("H:i:s");
$time_close = $hub->getTimeClose()->format("H:i:s");
$filter_time = $date_time->format("H:i:s");
if (($filter_time >= $time_open) &&
($filter_time <= $time_close))
{
$results[] = [
'hub' => $hub,
'db_distance' => $hub_data['db_distance'],
'distance' => $hub_data['distance'],
'duration' => $hub_data['duration'],
'jo_count' => 0,
];
}
else
$this->hub_filter_logger->logFilteredHub($hub, 'date_and_time', $jo_id, $customer_id);
}
return $results;
}
protected function filterHubsByInventory($hubs, $flag_inventory_check, $jo_type, $items, $jo_id, $customer_id)
{
if (empty($hubs))
return $hubs;
if (!$flag_inventory_check)
return $hubs;
$results = [];
if ($flag_inventory_check)
{
foreach ($hubs as $hub_data)
{
$hub = $hub_data['hub'];
if ($jo_type == ServiceType::BATTERY_REPLACEMENT_NEW)
{
// call inventory
$has_items = $this->checkInventory($items, $hub);
if ($has_items)
$results[] = [
'hub' => $hub,
'db_distance' => $hub_data['db_distance'],
'distance' => $hub_data['distance'],
'duration' => $hub_data['duration'],
'jo_count' => 0,
];
else
{
// get the skus for the message
$sku_text = '';
foreach ($items as $key => $value)
{
$sku_text .= ' ' . $key;
}
// send SMS to hub
$message = str_replace('item_display', trim($sku_text), $this->trans->trans('no_inventory_message'));
// error_log($message);
$this->sendSMSMessage($hub, $items);
$this->hub_filter_logger->logFilteredHub($hub, 'no_inventory', $jo_id, $customer_id);
}
}
if ($jo_type == ServiceType::BATTERY_REPLACEMENT_WARRANTY)
{
// call inventory
$has_items = $this->checkInventory($items, $hub);
if ($has_items)
$results[] = [
'hub' => $hub,
'db_distance' => $hub_data['db_distance'],
'distance' => $hub_data['distance'],
'duration' => $hub_data['duration'],
'jo_count' => 0,
];
else
{
// get the skus for the message
$sku_text = '';
foreach ($items as $key => $value)
{
$sku_text .= ' ' . $key;
}
// send SMS to hub
$message = str_replace('item_display', trim($sku_text), $this->trans->trans('no_inventory_message'));
// error_log($message);
$this->sendSMSMessage($hub, $items);
$this->hub_filter_logger->logFilteredHub($hub, 'no_inventory', $jo_id, $customer_id);
}
}
}
}
return $results;
return $filtered_hubs;
}
protected function getClosestHubs(Point $point, $limit_distance, $jo_id, $customer_id)
@ -375,7 +166,11 @@ class HubSelector
'distance' => $dist,
'duration' => 0,
'jo_count' => 0,
'inventory' => 0,
];
// log to file
$this->logClosestHubResult($jo_id, $row[0], $dist, $limit_distance);
}
else
{
@ -386,45 +181,6 @@ class HubSelector
return $hubs_data;
}
protected function checkInventory($items, $hub)
{
// check if hub has all items
$skus = [];
$branch_codes[] = $hub->getBranchCode();
$result = false;
foreach ($items as $key=> $value)
{
// add sap code of item/battery into array since
// getBranchesInventory takes in an array of hubs/branches
// and an array of skus
// $items as format: $items[sku] = quantity
$skus[] = $key;
}
// call InventoryManager's getBranchesInventory to check if hub has all items
$branches_with_items = $this->im->getBranchesInventory($branch_codes, $skus);
if (!empty($branches_with_items))
{
// check if branch has enough quantity for item
foreach ($branches_with_items as $branch)
{
// get quantity from call
$qty_available = $branch['Quantity'];
// get the quantity request
$sku_requested = $branch['SapCode'];
$qty_requested = $items[$sku_requested];
if ($qty_available >= $qty_requested)
$result = true;
}
}
// return true or false
return $result;
}
// convert db distance to kilometers
protected function distance($lat1, $lon1, $lat2, $lon2)
{
@ -440,26 +196,50 @@ class HubSelector
return round(($miles * 1.609344), 1);
}
protected function sendSMSMessage($hub, $items)
protected function isExemptedByArea(string $filter_id, Point $coordinates): bool
{
// compose message
// get the skus for the message
$sku_text = '';
foreach ($items as $key => $value)
{
$sku_text .= ' ' . $key;
}
$message = str_replace('item_display', trim($sku_text), $this->trans->trans('no_inventory_message'));
$long = $coordinates->getLongitude();
$lat = $coordinates->getLatitude();
// get hub notification number
$mobile_number = $hub->getNotifNumber();
// get supported area given a set of coordinates
$query = $this->em->createQuery('SELECT s from App\Entity\SupportedArea s where st_contains(s.coverage_area, point(:long, :lat)) = true');
$area = $query->setParameter('long', $long)
->setParameter('lat', $lat)
->setMaxResults(1)
->getOneOrNullResult();
if (!empty($mobile_number))
{
// send SMS message
// error_log('sending sms to - ' . $mobile_number);
$this->rt->sendSMS($mobile_number, $this->trans->trans('message.battery_brand_allcaps'), $message);
}
if ($area !== null) {
// get all exceptions
$exceptions = $area->getHubFilterExceptions();
if (isset($exceptions[$filter_id])) {
error_log("FILTER " . $filter_id . " DISABLED FOR AREA: " . $area->getName());
// disable this filter for this area
return true;
}
}
// filter is in place
return false;
}
protected function logClosestHubResult($jo_id, $hub, $distance, $limit_distance)
{
// log to file
$filename = '/../../var/log/closest_hubs_selected.log';
$date = date("Y-m-d H:i:s");
// build log entry
$entry = implode("", [
"[JO: " . $jo_id . "]",
"[" . $date . "]",
"[Distance: " . $distance . " vs " . $limit_distance . "]",
" " . $hub->getName() . " (ID: " . $hub->getID() . ")",
"\r\n",
]);
@file_put_contents(__DIR__ . $filename, $entry, FILE_APPEND);
}
}

View file

@ -91,7 +91,7 @@ class InsuranceConnector
return base64_encode($this->username . ":" . $this->password);
}
protected function doRequest($url, $method, $body = [])
protected function doRequest($url, $method, $request_body = [])
{
$client = new Client();
$headers = [
@ -102,7 +102,7 @@ class InsuranceConnector
try {
$response = $client->request($method, $this->base_url . '/' . $url, [
'json' => $body,
'json' => $request_body,
'headers' => $headers,
]);
} catch (RequestException $e) {
@ -111,6 +111,11 @@ class InsuranceConnector
error_log("Insurance API Error: " . $error['message']);
error_log(Psr7\Message::toString($e->getRequest()));
error_log($e->getResponse()->getBody()->getContents());
error_log("Insurance Creds: " . $this->username . ", " . $this->password);
error_log("Insurance Hash: " . $this->generateHash());
// log this error
$this->log($url, Psr7\Message::toString($e->getRequest()), Psr7\Message::toString($e->getResponse()), 'error');
if ($e->hasResponse()) {
$error['response'] = Psr7\Message::toString($e->getResponse());
@ -122,11 +127,32 @@ class InsuranceConnector
];
}
error_log(print_r(json_decode($response->getBody(), true), true));
$result_body = $response->getBody();
// log response
$this->log($url, json_encode($request_body), $result_body);
return [
'success' => true,
'response' => json_decode($response->getBody(), true)
'response' => json_decode($result_body, true),
];
}
// TODO: make this more elegant
public function log($title, $request_body = "[]", $result_body = "[]", $type = 'api')
{
$filename = '/../../var/log/insurance_' . $type . '.log';
$date = date("Y-m-d H:i:s");
// build log entry
$entry = implode("\r\n", [
$date,
$title,
"REQUEST:\r\n" . $request_body,
"RESPONSE:\r\n" . $result_body,
"\r\n----------------------------------------\r\n\r\n",
]);
@file_put_contents(__DIR__ . $filename, $entry, FILE_APPEND);
}
}

View file

@ -134,7 +134,7 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface
}
// generate invoice criteria
public function generateInvoiceCriteria($jo, $discount, $invoice_items, $source = null, &$error_array)
public function generateInvoiceCriteria($jo, $discount, $invoice_items, $price_tier = null, $source = null, &$error_array)
{
$em = $this->em;

View file

@ -144,7 +144,7 @@ class ResqInvoiceGenerator implements InvoiceGeneratorInterface
}
// generate invoice criteria
public function generateInvoiceCriteria($jo, $promo_id, $invoice_items, $source = null, &$error_array)
public function generateInvoiceCriteria($jo, $promo_id, $invoice_items, $price_tier = null, $source = null, &$error_array)
{
$em = $this->em;

View file

@ -4,6 +4,7 @@ namespace App\Service;
use App\Entity\Invoice;
use App\Entity\JobOrder;
use App\Entity\PriceTier;
use App\Ramcar\InvoiceCriteria;
@ -13,7 +14,7 @@ interface InvoiceGeneratorInterface
public function generateInvoice(InvoiceCriteria $criteria);
// generate invoice criteria
public function generateInvoiceCriteria(JobOrder $jo, int $promo_id, array $invoice_items, $source, array &$error_array);
public function generateInvoiceCriteria(JobOrder $jo, int $promo_id, array $invoice_items, $source, PriceTier $price_tier, array &$error_array);
// prepare draft for invoice
public function generateDraftInvoice(InvoiceCriteria $criteria, int $promo_id, array $service_charges, array $items);

View file

@ -10,6 +10,7 @@ use Doctrine\ORM\EntityManagerInterface;
use App\InvoiceRule;
use App\Service\InvoiceGeneratorInterface;
use App\Service\PriceTierManager;
use App\Ramcar\InvoiceCriteria;
use App\Ramcar\InvoiceStatus;
@ -28,12 +29,14 @@ class InvoiceManager implements InvoiceGeneratorInterface
protected $em;
protected $validator;
protected $available_rules;
protected $pt_manager;
public function __construct(EntityManagerInterface $em, Security $security, ValidatorInterface $validator)
public function __construct(EntityManagerInterface $em, Security $security, ValidatorInterface $validator, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->security = $security;
$this->validator = $validator;
$this->pt_manager = $pt_manager;
$this->available_rules = $this->getAvailableRules();
}
@ -42,28 +45,37 @@ class InvoiceManager implements InvoiceGeneratorInterface
{
// TODO: get list of invoice rules from .env or a json file?
return [
new InvoiceRule\BatterySales($this->em),
new InvoiceRule\BatteryReplacementWarranty($this->em),
new InvoiceRule\Jumpstart($this->em),
new InvoiceRule\JumpstartWarranty($this->em),
new InvoiceRule\PostRecharged($this->em),
new InvoiceRule\PostReplacement($this->em),
new InvoiceRule\Overheat($this->em),
new InvoiceRule\Fuel($this->em),
new InvoiceRule\TireRepair($this->em),
new InvoiceRule\BatterySales($this->em, $this->pt_manager),
new InvoiceRule\BatteryReplacementWarranty($this->em, $this->pt_manager),
new InvoiceRule\Jumpstart($this->em, $this->pt_manager),
new InvoiceRule\JumpstartWarranty($this->em, $this->pt_manager),
new InvoiceRule\PostRecharged($this->em, $this->pt_manager),
new InvoiceRule\PostReplacement($this->em, $this->pt_manager),
new InvoiceRule\Overheat($this->em, $this->pt_manager),
new InvoiceRule\Fuel($this->em, $this->pt_manager),
new InvoiceRule\TireRepair($this->em, $this->pt_manager),
new InvoiceRule\DiscountType($this->em),
new InvoiceRule\TradeIn(),
new InvoiceRule\Tax($this->em),
new InvoiceRule\TradeIn($this->em),
new InvoiceRule\Tax($this->em, $this->pt_manager),
];
}
// this is called when JO is submitted
public function generateInvoiceCriteria($jo, $promo_id, $invoice_items, $source, &$error_array)
public function generateInvoiceCriteria($jo, $promo_id, $invoice_items, $source, $price_tier, &$error_array)
{
// instantiate the invoice criteria
$criteria = new InvoiceCriteria();
$criteria->setServiceType($jo->getServiceType())
->setCustomerVehicle($jo->getCustomerVehicle());
->setCustomerVehicle($jo->getCustomerVehicle())
->setPriceTier($price_tier);
if (($jo->getServiceType() == ServiceType::OVERHEAT_ASSISTANCE) &&
($jo->hasCoolant()))
$criteria->setHasCoolant(true);
if (($jo->getServiceType() == ServiceType::TIRE_REPAIR) &&
($jo->hasSealant()))
$criteria->setHasSealant(true);
// set if taxable
// NOTE: ideally, this should be a parameter when calling generateInvoiceCriteria. But that
@ -162,6 +174,7 @@ class InvoiceManager implements InvoiceGeneratorInterface
// (3) generateInvoiceCriteria
// (4) RiderAPIHandler's changeService
// (5) TAPI's JobOrderController
// (6) CAPI's RiderAppController
public function generateInvoice($criteria)
{
// no need to validate since generateDraftInvoice was called before this was called
@ -210,17 +223,27 @@ class InvoiceManager implements InvoiceGeneratorInterface
$price = $item['price'];
$battery = null;
$battery_size = null;
$trade_in_type = '';
if (isset($item['battery']))
$battery = $item['battery'];
if (isset($item['promo']))
$promo = $item['promo'];
if (isset($item['battery_size']))
$battery_size = $item['battery_size'];
if (isset($item['trade_in_type']))
$trade_in_type = $item['trade_in_type'];
$invoice_items[] = [
'title' => $title,
'quantity' => $quantity,
'price' => $price,
'battery' => $battery,
'battery_size' => $battery_size,
'trade_in_type' => $trade_in_type,
];
}
}
@ -269,11 +292,15 @@ class InvoiceManager implements InvoiceGeneratorInterface
$invoice_item->setInvoice($invoice)
->setTitle($item['title'])
->setQuantity($item['quantity'])
->setPrice((float)$item['price']);
->setPrice((float)$item['price'])
->setTradeInType($item['trade_in_type']);
if ($item['battery'] != null)
$invoice_item->setBattery($item['battery']);
if ($item['battery_size'] != null)
$invoice_item->setBatterySize($item['battery_size']);
$invoice->addItem($invoice_item);
}

View file

@ -30,6 +30,7 @@ use App\Entity\EmergencyType;
use App\Entity\OwnershipType;
use App\Entity\CustomerLocation;
use App\Entity\Battery;
use App\Entity\BatterySize;
use App\Ramcar\ServiceType;
use App\Ramcar\TradeInType;
@ -69,6 +70,7 @@ use App\Service\HubSelector;
use App\Service\HubDistributor;
use App\Service\HubFilteringGeoChecker;
use App\Service\JobOrderManager;
use App\Service\PriceTierManager;
use CrEOF\Spatial\PHP\Types\Geometry\Point;
@ -94,8 +96,9 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
protected $hub_dist;
protected $hub_geofence;
protected $cust_distance_limit;
protected $hub_filter_enable;
protected $hub_filter_enabled;
protected $jo_manager;
protected $pt_manager;
protected $template_hash;
@ -104,7 +107,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
TranslatorInterface $translator, RiderAssignmentHandlerInterface $rah,
string $country_code, WarrantyHandler $wh, RisingTideGateway $rt,
PromoLogger $promo_logger, HubDistributor $hub_dist, HubFilteringGeoChecker $hub_geofence,
string $cust_distance_limit, string $hub_filter_enabled, JobOrderManager $jo_manager)
string $cust_distance_limit, string $hub_filter_enabled, JobOrderManager $jo_manager, PriceTierManager $pt_manager)
{
$this->em = $em;
$this->ic = $ic;
@ -121,6 +124,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
$this->cust_distance_limit = $cust_distance_limit;
$this->hub_filter_enabled = $hub_filter_enabled;
$this->jo_manager = $jo_manager;
$this->pt_manager = $pt_manager;
$this->loadTemplates();
}
@ -585,7 +589,9 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
{
$source = $jo->getSource();
$this->ic->generateInvoiceCriteria($jo, $promo_id, $invoice_items, $source, $error_array);
// get the price tier according to location.
$price_tier = $this->pt_manager->getPriceTier($jo->getCoordinates());
$this->ic->generateInvoiceCriteria($jo, $promo_id, $invoice_items, $source, $price_tier, $error_array);
}
// validate
@ -817,7 +823,9 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
{
$source = $obj->getSource();
$this->ic->generateInvoiceCriteria($obj, $promo_id, $invoice_items, $source, $error_array);
// get the price tier according to location.
$price_tier = $this->pt_manager->getPriceTier($obj->getCoordinates());
$this->ic->generateInvoiceCriteria($obj, $promo_id, $invoice_items, $source, $price_tier, $error_array);
}
// validate
@ -1604,6 +1612,26 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
$error_array['cust_location'] = 'Invalid customer location';
}
// check facilitated type
$fac_type = $req->request->get('facilitated_type');
if (!empty($fac_type))
{
if (!FacilitatedType::validate($fac_type))
$fac_type = null;
}
else
$fac_type = null;
// check facilitated by
$fac_by_id = $req->request->get('facilitated_by');
$fac_by = null;
if (!empty($fac_by_id))
{
$fac_by = $em->getRepository(Hub::class)->find($fac_by_id);
if (empty($fac_by))
$fac_by = null;
}
// get previously assigned hub, if any
$old_hub = $obj->getHub();
@ -1664,6 +1692,8 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
->setOwnershipType($owner_type)
->setCustomerLocation($cust_location)
->setInventoryCount($req->request->get('hub_inv_count', 0))
->setFacilitatedType($fac_type)
->setFacilitatedBy($fac_by)
->clearRider();
if ($user != null)
@ -2014,6 +2044,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
// NOTE: for resq2 app
$mclientv2->sendEvent($obj, $payload);
$mclientv2->sendRiderEvent($obj, $payload);
$fcmclient->sendJoEvent($obj, "jo_fcm_title_driver_assigned", "jo_fcm_body_driver_assigned");
}
@ -2165,7 +2196,9 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
// NOTE: this is CMB code but for compilation purposes we need to add this
$source = $jo->getSource();
$this->ic->generateInvoiceCriteria($jo, $promo_id, $invoice_items, $source, $error_array);
// get the price tier according to location.
$price_tier = $this->pt_manager->getPriceTier($jo->getCoordinates());
$this->ic->generateInvoiceCriteria($jo, $promo_id, $invoice_items, $source, $price_tier, $error_array);
}
// validate
@ -2302,6 +2335,13 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
$em = $this->em;
$jo = $em->getRepository(JobOrder::class)->find($id);
// get the job order's invoice items
$invoice = $jo->getInvoice();
$invoice_items = [];
if ($invoice != null)
$invoice_items = $invoice->getItems();
$params['invoice_items'] = $invoice_items;
$params['obj'] = $jo;
$params['mode'] = 'open_edit';
$params['cvid'] = $jo->getCustomerVehicle()->getID();
@ -2541,10 +2581,20 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
->setDateTime($obj->getDateSchedule())
->setLimitResults(50);
// NOTE: set JO type regardless, for now
$hub_criteria->setJoType($obj->getServiceType())
->setOrderDate($obj->getDateCreate())
->setServiceType($obj->getServiceType());
// set customer class
$cust = $obj->getCustomer();
$hub_criteria->setCustomerClass($cust->getCustomerClassification());
// check if hub filter is enabled. If not, use default values
// for the rest of the HubCriteria fields
if ($this->hub_filter_enabled == 'true')
{
// TODO: allow this to be disabled via env or CRM. commenting out for now
// error_log('hub filter is enabled');
if ($this->hub_geofence->isCovered($long, $lat))
{
@ -2552,7 +2602,6 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
// error_log('Area is covered by hub filtering');
$hub_criteria->setLimitDistance($this->cust_distance_limit)
->setPaymentMethod($obj->getModeOfPayment())
->setJoType($obj->getServiceType())
->setRoundRobin(true);
}
}
@ -2563,15 +2612,22 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
{
// reset distance limit if emergency
//TODO: move to .env the emergency distance limit
$hub_criteria->setLimitDistance(500);
$hub_criteria->setEmergency(true);
$hub_criteria->setLimitDistance(500)
->setEmergency(true)
->setPaymentMethod(null)
->setRoundRobin(false);
}
// set filter flags for inventory and available riders
$hub_criteria->setInventoryCheck();
$hub_criteria->setRidersCheck();
// get JO and customer id for logging purposes
$jo_id = $obj->getID();
$customer_id = $obj->getCustomer()->getID();
$hub_criteria->setJobOrderId($jo_id)
->setJoOrigin($obj->getSource())
->setCustomerId($customer_id);
$hubs = $hub_selector->find($hub_criteria);
@ -2636,7 +2692,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
// handle inventory data
$bcode = $hub['hub']->getBranchCode();
$hub['inventory'] = 0;
//$hub['inventory'] = 0;
if ($bcode != '')
{
$branch_codes[] = $bcode;
@ -2650,9 +2706,13 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
$params['hubs'][$hub_id] = $hub;
}
$params['obj'] = $obj;
// get template to display
$params['template'] = $this->getTwigTemplate('jo_processing_form');
// get all enabled filters
$enabled_filter_str = $_ENV['ENABLED_HUB_FILTERS'];
$enabled_filters = explode(",", $enabled_filter_str);
// if inventory filter is disabled, fetch inventory here
if (!in_array('InventoryHubFilter', $enabled_filters) || $this->skipInventoryCheck($obj->getCoordinates())) {
error_log("NO INVENTORY CHECKS, GETTING INVENTORY FOR JO " . $obj->getID());
// get battery (if any)
$skus = [];
@ -2669,6 +2729,10 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
// get inventory
$mres = $motiv->getInventory($branch_codes, $skus);
$x = 0;
error_log("TOTAL RESULTS FROM MOTIV: " . count($mres) . " OUT OF " . count($branch_codes) . " BRANCH CODES AND " . count($skus) . " SKUS");
foreach ($mres as $mres_item)
{
// check if we have a valid response from motiv, ignore otherwise
@ -2681,11 +2745,25 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
$hub_id = $inv_data[$bcode]['hub_id'];
$params['hubs'][$hub_id]['inventory'] = $inv_count;
error_log("SETTING HUB " . $hub_id . " INVENTORY TO " . $inv_count);
$x++;
} else {
error_log("CANNOT FIND BCODE FOR " . $bcode);
}
} else {
error_log("CANNOT FIND BCODE FOR RESULT: " . print_r($mres_item, true));
}
}
error_log("SET QUANTITY OF " . $x . " HUBS TO NON ZERO");
// error_log(print_r($mres, true));
}
$params['obj'] = $obj;
// get template to display
$params['template'] = $this->getTwigTemplate('jo_processing_form');
return $params;
}
@ -2879,13 +2957,22 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
->setDateTime($obj->getDateSchedule())
->setLimitResults(50);
// NOTE: set JO type regardless, for now
$hub_criteria->setJoType($obj->getServiceType())
->setOrderDate($obj->getDateCreate())
->setServiceType($obj->getServiceType());
// set customer class
$cust = $obj->getCustomer();
$hub_criteria->setCustomerClass($cust->getCustomerClassification());
// TODO: allow this to be disabled via env or CRM. commenting out for now
if ($this->hub_geofence->isCovered($long, $lat))
{
// if true, set other values for HubCriteria
// error_log('Area is covered by hub');
$hub_criteria->setLimitDistance($this->cust_distance_limit)
->setPaymentMethod($obj->getModeOfPayment())
->setJoType($obj->getServiceType())
->setRoundRobin(true);
}
@ -2894,15 +2981,22 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
if ($willing_to_wait == WillingToWaitContent::NOT_WILLING_TO_WAIT)
{
//TODO: move to .env the emergency distance limit
$hub_criteria->setLimitDistance(500);
$hub_criteria->setEmergency(true);
$hub_criteria->setLimitDistance(500)
->setEmergency(true)
->setPaymentMethod(null)
->setRoundRobin(false);
}
// set filter flags for inventory and available riders
$hub_criteria->setInventoryCheck();
$hub_criteria->setRidersCheck();
// get JO and customer id for logging purposes
$jo_id = $obj->getID();
$customer_id = $obj->getCustomer()->getID();
$customer_id = $cust->getID();
$hub_criteria->setJobOrderId($jo_id)
->setJoOrigin($obj->getSource())
->setCustomerId($customer_id);
$hubs = $hub_selector->find($hub_criteria);
@ -2910,6 +3004,9 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
$params['status_cancelled'] = JOStatus::CANCELLED;
$params['hubs'] = [];
$branch_codes = [];
$inv_data = [];
// format duration and distance into friendly time
foreach ($hubs as $hub) {
// duration
@ -2965,7 +3062,7 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
// handle inventory data
$bcode = $hub['hub']->getBranchCode();
$hub['inventory'] = 0;
//$hub['inventory'] = 0;
if ($bcode != '')
{
$branch_codes[] = $bcode;
@ -2979,6 +3076,14 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
$params['hubs'][$hub_id] = $hub;
}
// get all enabled filters
$enabled_filter_str = $_ENV['ENABLED_HUB_FILTERS'];
$enabled_filters = explode(",", $enabled_filter_str);
// if inventory filter is disabled, fetch inventory here
if (!in_array('InventoryHubFilter', $enabled_filters) || $this->skipInventoryCheck($obj->getCoordinates())) {
error_log("NO INVENTORY CHECKS, GETTING INVENTORY FOR JO " . $obj->getID());
// get battery (if any)
$skus = [];
$invoice = $obj->getInvoice();
@ -2994,6 +3099,10 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
// get inventory
$mres = $motiv->getInventory($branch_codes, $skus);
$x = 0;
error_log("TOTAL RESULTS FROM MOTIV: " . count($mres) . " OUT OF " . count($branch_codes) . " BRANCH CODES AND " . count($skus) . " SKUS");
foreach ($mres as $mres_item)
{
// check if we have a valid response from motiv, ignore otherwise
@ -3006,11 +3115,21 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
$hub_id = $inv_data[$bcode]['hub_id'];
$params['hubs'][$hub_id]['inventory'] = $inv_count;
error_log("SETTING HUB " . $hub_id . " INVENTORY TO " . $inv_count);
$x++;
} else {
error_log("CANNOT FIND BCODE FOR " . $bcode);
}
} else {
error_log("CANNOT FIND BCODE FOR RESULT: " . print_r($mres_item, true));
}
}
error_log("SET QUANTITY OF " . $x . " HUBS TO NON ZERO");
// error_log(print_r($mres, true));
}
$params['obj'] = $obj;
// get template to display
@ -3542,15 +3661,15 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
$params['trade_in_bmfgs'] = $em->getRepository(BatteryManufacturer::class)->findAll();
$params['promos'] = $em->getRepository(Promo::class)->findAll();
// list of batteries for trade-in
$ti_batteries = $em->getRepository(Battery::class)->findAll();
$trade_in_batteries = [];
foreach ($ti_batteries as $ti_battery)
// list of battery sizes for trade-in
$ti_batt_sizes = $em->getRepository(BatterySize::class)->findAll();
$trade_in_batt_sizes = [];
foreach ($ti_batt_sizes as $ti_batt_size)
{
$battery_name = $ti_battery->getModel()->getName() . ' ' . $ti_battery->getSize()->getName();
$trade_in_batteries[$ti_battery->getID()] = $battery_name;
$batt_size_name = $ti_batt_size->getName();
$trade_in_batt_sizes[$ti_batt_size->getID()] = $batt_size_name;
}
$params['trade_in_batteries'] = $trade_in_batteries;
$params['trade_in_batt_sizes'] = $trade_in_batt_sizes;
// list of emergency types
$e_types = $em->getRepository(EmergencyType::class)->findBy([], ['name' => 'ASC']);
@ -4248,8 +4367,10 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
$phone_number = $this->country_code . $notif_number;
// check if reason is administrative
if ($rejection->getReason() == JORejectionReason::ADMINISTRATIVE)
// check if reason is in the list of rejection reason
$flag_send_sms = $this->checkRejectionReason($rejection->getReason());
if (!$flag_send_sms)
return null;
// sms content
@ -4283,4 +4404,65 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
$this->rt->sendSMS($phone_number, $this->translator->trans('message.battery_brand_allcaps'), $msg);
}
protected function checkRejectionReason($reason): bool
{
// reason is administrative
if ($reason == JORejectionReason::ADMINISTRATIVE)
return false;
// reason is discount
if ($reason == JORejectionReason::DISCOUNT)
return false;
// reason is store closed on schedule
if ($reason == JORejectionReason::STORE_CLOSED_SCHEDULED)
return false;
// store closed half day
if ($reason == JORejectionReason::STORE_CLOSED_HALF_DAY)
return false;
// prio hub for warranty claim
if ($reason == JORejectionReason::PRIORITY_HUB_WTY_CLAIM)
return false;
// prio hub for jumpstart
if ($reason == JORejectionReason::PRIORITY_HUB_JUMPSTART)
return false;
// prio hub for RES-Q request
if ($reason == JORejectionReason::PRIORITY_HUB_RESQ_REQUEST)
return false;
// customer request
if ($reason == JORejectionReason::CUSTOMER_REQUEST)
return false;
return true;
}
protected function skipInventoryCheck(Point $coordinates): bool
{
$long = $coordinates->getLongitude();
$lat = $coordinates->getLatitude();
// get supported area given a set of coordinates
$query = $this->em->createQuery('SELECT s from App\Entity\SupportedArea s where st_contains(s.coverage_area, point(:long, :lat)) = true');
$area = $query->setParameter('long', $long)
->setParameter('lat', $lat)
->setMaxResults(1)
->getOneOrNullResult();
if ($area !== null) {
// get all exceptions
$exceptions = $area->getHubFilterExceptions();
if (isset($exceptions['no_inventory'])) {
return true;
}
}
// filter is in place
return false;
}
}

View file

@ -68,4 +68,31 @@ class MQTTClientApiv2
// error_log('sent to ' . $channel);
}
}
public function sendRiderEvent(JobOrder $job_order, $payload)
{
// check if a rider is available
$rider = $job_order->getRider();
if ($rider == null)
return;
/*
// NOTE: this is for the old rider app
// check if rider has sessions
$sessions = $rider->getSessions();
if (count($sessions) == 0)
return;
// send to every rider session
foreach ($sessions as $sess)
{
$sess_id = $sess->getID();
$channel = self::RIDER_PREFIX . $sess_id;
$this->publish($channel, json_encode($payload));
}
*/
// NOTE: this is for the new rider app
$this->publish('rider/' . $rider->getID() . '/delivery', json_encode($payload));
}
}

View file

@ -74,12 +74,22 @@ class PayMongoConnector
return $this->doRequest('/v1/checkout_sessions/' . $checkout_id, 'GET');
}
public function getWebhook($id)
{
return $this->doRequest('/v1/webhooks/'. $id, 'GET');
}
public function enableWebhook($id)
{
return $this->doRequest('/v1/webhooks/' . $id . '/enable', 'POST');
}
protected function generateHash()
{
return base64_encode($this->secret_key);
}
protected function doRequest($url, $method, $body = [])
protected function doRequest($url, $method, $request_body = [])
{
$client = new Client();
$headers = [
@ -90,14 +100,14 @@ class PayMongoConnector
try {
$response = $client->request($method, $this->base_url . '/' . $url, [
'json' => $body,
'json' => $request_body,
'headers' => $headers,
]);
} catch (RequestException $e) {
$error = ['message' => $e->getMessage()];
ob_start();
var_dump($body);
//var_dump($request_body);
$varres = ob_get_clean();
error_log($varres);
@ -107,6 +117,9 @@ class PayMongoConnector
error_log("PayMongo API Error: " . $error['message']);
error_log(Psr7\Message::toString($e->getRequest()));
// log this error
$this->log($url, Psr7\Message::toString($e->getRequest()), Psr7\Message::toString($e->getResponse()), 'error');
if ($e->hasResponse()) {
$error['response'] = Psr7\Message::toString($e->getResponse());
}
@ -117,9 +130,32 @@ class PayMongoConnector
];
}
$result_body = $response->getBody();
// log response
$this->log($url, json_encode($request_body), $result_body);
return [
'success' => true,
'response' => json_decode($response->getBody(), true)
];
}
// TODO: make this more elegant
public function log($title, $request_body = "[]", $result_body = "[]", $type = 'api')
{
$filename = '/../../var/log/paymongo_' . $type . '.log';
$date = date("Y-m-d H:i:s");
// build log entry
$entry = implode("\r\n", [
$date,
$title,
"REQUEST:\r\n" . $request_body,
"RESPONSE:\r\n" . $result_body,
"\r\n----------------------------------------\r\n\r\n",
]);
@file_put_contents(__DIR__ . $filename, $entry, FILE_APPEND);
}
}

View file

@ -0,0 +1,80 @@
<?php
namespace App\Service;
use Doctrine\ORM\EntityManagerInterface;
use CrEOF\Spatial\PHP\Types\Geometry\Point;
use App\Entity\PriceTier;
class PriceTierManager
{
protected $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function getItemPrice($pt_id, $item_type_id, $item_id)
{
// find the item price, given the price tier, battery id, and item type (battery)
$db_conn = $this->em->getConnection();
$ip_sql = 'SELECT ip.price AS price
FROM item_price ip
WHERE ip.price_tier_id = :pt_id
AND ip.item_type_id = :it_id
AND ip.item_id = :item_id';
$ip_stmt = $db_conn->prepare($ip_sql);
$ip_stmt->bindValue('pt_id', $pt_id);
$ip_stmt->bindValue('it_id', $item_type_id);
$ip_stmt->bindValue('item_id', $item_id);
$ip_result = $ip_stmt->executeQuery();
// results found
$actual_price = null;
// go through rows
while ($row = $ip_result->fetchAssociative())
{
// get the price
$price = $row['price'];
// actual price
$actual_price = number_format($price / 100, 2, '.', '');
}
return $actual_price;
}
public function getPriceTier(Point $coordinates)
{
$price_tier_id = 0;
if ($coordinates != null)
{
$long = $coordinates->getLongitude();
$lat = $coordinates->getLatitude();
// get location's price tier, given a set of coordinates
$query = $this->em->createQuery('SELECT s from App\Entity\SupportedArea s where st_contains(s.coverage_area, point(:long, :lat)) = true');
$area = $query->setParameter('long', $long)
->setParameter('lat', $lat)
->setMaxResults(1)
->getOneOrNullResult();
if ($area != null)
{
$price_tier = $area->getPriceTier();
if ($price_tier != null)
$price_tier_id = $price_tier->getID();
}
}
return $price_tier_id;
}
}

View file

@ -8,11 +8,11 @@
</select>
</div>
<div class="col-lg-3">
<label for="invoice-trade-in-battery">Battery For Trade In</label>
<select class="form-control m-input" id="invoice-trade-in-battery" data-value="">
<label for="invoice-trade-in-battery-size">Battery Size For Trade In</label>
<select class="form-control m-input" id="invoice-trade-in-battery-size" data-value="">
<option value=""></option>
{% for id, battery_name in trade_in_batteries %}
<option value="{{ id }}">{{ battery_name }}</option>
{% for id, batt_size_name in trade_in_batt_sizes %}
<option value="{{ id }}">{{ batt_size_name }}</option>
{% endfor %}
</select>
</div>

View file

@ -1,13 +1,13 @@
// add trade in battery to invoice
$('#btn-add-trade-in-to-invoice').click(function() {
var bmfg = $("#invoice-trade-in-bmfg").val();
var battery = $("#invoice-trade-in-battery").val();
var battery_size = $("#invoice-trade-in-battery-size").val();
var tradeIn = $("#invoice-trade-in-type").val();
var qty = $("#invoice-trade-in-quantity").val();
// add to invoice array
invoiceItems.push({
battery: battery,
battery_size: battery_size,
quantity: qty,
trade_in: tradeIn,
});

View file

@ -0,0 +1,164 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">Item Pricing</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<div class="row">
<div class="col-xl-12">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__body">
<div class="m-form m-form--label-align-right m--margin-top-20 m--margin-bottom-30">
<div class="row align-items-center">
<div class="col-xl-12">
<div class="form-group m-form__group row align-items-center">
<div class="col-md-2">
<label>Item Prices for </label>
</div>
<div class="col-md-3">
<div class="m-input-icon m-input-icon--left">
<div class="input-group">
<select class="form-control m-input" id="price-tier-select" name="price_tier_list">
<option value="0">Default Price Tier</option>
{% for price_tier in sets.price_tiers %}
<option value="{{ price_tier.getID }}">{{ price_tier.getName }} </option>
{% endfor %}
</select>
</div>
</div>
</div>
<div class="col-md-3">
<div class="m-input-icon m-input-icon--left">
<div class="input-group">
<select class="form-control m-input" id="item-type-select" name="item_type_list">
{% for item_type in sets.item_types %}
<option value="{{ item_type.getID }}">{{ item_type.getName }} </option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<form id="row-form" class="m-form m-form--fit m-form--label-align-right" method="post" action="{{ url('item_pricing_update') }}">
<input id="price-tier-id" type="hidden" name="price_tier_id" value="0">
<input id="item-type-id" type="hidden" name="item_type_id" value="{{ default_item_type_id }}">
<div style="padding-left: 25px; padding-right: 25px;">
<table class="table">
<thead>
<tr>
<th style="width: 100px">ID</th>
<th>Name</th>
<th hidden> Item Type ID </th>
<th>Item Type</th>
<th style="width: 180px">Price</th>
</tr>
</thead>
<tbody id="table-body">
{% for id, item in items.items %}
<tr>
<td>{{ id }}</td>
<td>{{ item.name }} </td>
<td hidden> {{ item.item_type_id }} </td>
<td>{{ item.item_type }} </td>
<td class="py-1">
<input name="price[{{ id }}]" class="form-control ca-filter" type="number" value="{{ item.price }}" step="0.01">
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="">
<input type="submit" class="btn btn-primary" value="Update Price">
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
initialize();
function initialize() {
init_price_tier_dropdown();
init_item_type_dropdown();
}
function init_price_tier_dropdown() {
var pt_dropdown = document.getElementById('price-tier-select');
var it_dropdown = document.getElementById('item-type-select');
pt_dropdown.addEventListener('change', function(e) {
var it_type = it_dropdown.value;
load_prices(e.target.value, it_type);
});
}
function init_item_type_dropdown() {
var it_dropdown = document.getElementById('item-type-select');
var pt_dropdown = document.getElementById('price-tier-select');
it_dropdown.addEventListener('change', function(e) {
var pt_type = pt_dropdown.value;
load_prices(pt_type, e.target.value);
});
}
function load_prices(price_tier_id, item_type_id) {
var req = new XMLHttpRequest();
req.onreadystatechange = function() {
// process response
if (this.readyState == 4 && this.status == 200) {
// update form
update_table(JSON.parse(req.responseText));
var pt_field = document.getElementById('price-tier-id');
pt_field.value = price_tier_id;
var it_field = document.getElementById('item-type-id');
it_field.value = item_type_id;
} else {
// console.log('could not load tier prices');
}
}
var url_pattern = '{{ url('item_pricing_prices', {'pt_id': '--id--', 'it_id': '--it-id--'}) }}';
var url = url_pattern.replace('--id--', price_tier_id).replace('--it-id--', item_type_id);
console.log(url);
req.open('GET', url, true);
req.send();
}
function update_table(data) {
console.log(data);
var item_html = '';
for (var i in data.items) {
var item = data.items[i];
// console.log(item);
item_html += '<tr>';
item_html += '<td>' + item.id + '</td>';
item_html += '<td>' + item.name + '</td>';
item_html += '<td hidden>' + item.item_type_id + '</td>';
item_html += '<td>' + item.item_type + '</td>';
item_html += '<td class="py-1">';
item_html += '<input name="price[' + item.id + ']" class="form-control ca-filter" type="number" value="' + item.price + '" step="0.01">';
item_html += '</td>';
item_html += '</tr>';
}
var table_body = document.getElementById('table-body');
table_body.innerHTML = item_html;
}
</script>
{% endblock %}

View file

@ -0,0 +1,142 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">Item Types</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-6">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__head">
<div class="m-portlet__head-caption">
<div class="m-portlet__head-title">
<span class="m-portlet__head-icon">
<i class="la la-industry"></i>
</span>
<h3 class="m-portlet__head-text">
{% if mode == 'update' %}
Edit Item Type
<small>{{ obj.getName() }}</small>
{% else %}
New Item Type
{% endif %}
</h3>
</div>
</div>
</div>
<form id="row-form" class="m-form m-form--fit m-form--label-align-right m-form--group-seperator-dashed" method="post" action="{{ mode == 'update' ? url('item_type_update_submit', {'id': obj.getId()}) : url('item_type_add_submit') }}">
<div class="m-portlet__body">
<div class="form-group m-form__group row no-border">
<label class="col-lg-3 col-form-label" data-field="name">
Name:
</label>
<div class="col-lg-9">
<input type="text" name="name" class="form-control m-input" value="{{ obj.getName() }}">
<div class="form-control-feedback hide" data-field="name"></div>
</div>
</div>
<div class="form-group m-form__group row no-border">
<label class="col-lg-3 col-form-label" data-field="code">
Code:
</label>
<div class="col-lg-9">
<input type="text" name="code" class="form-control m-input" value="{{ obj.getCode() }}">
<div class="form-control-feedback hide" data-field="code"></div>
</div>
</div>
</div>
<div class="m-portlet__foot m-portlet__foot--fit">
<div class="m-form__actions m-form__actions--solid m-form__actions--right">
<div class="row">
<div class="col-lg-12">
<button type="submit" class="btn btn-success">Submit</button>
<a href="{{ url('item_type_list') }}" class="btn btn-secondary">Back</a>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
$("#row-form").submit(function(e) {
var form = $(this);
e.preventDefault();
$.ajax({
method: "POST",
url: form.prop('action'),
data: form.serialize()
}).done(function(response) {
// remove all error classes
removeErrors();
swal({
title: 'Done!',
text: 'Your changes have been saved!',
type: 'success',
onClose: function() {
window.location.href = "{{ url('item_type_list') }}";
}
});
}).fail(function(response) {
if (response.status == 422) {
var errors = response.responseJSON.errors;
var firstfield = false;
// remove all error classes first
removeErrors();
// display errors contextually
$.each(errors, function(field, msg) {
var formfield = $("[name='" + field + "']");
var label = $("label[data-field='" + field + "']");
var msgbox = $(".form-control-feedback[data-field='" + field + "']");
// add error classes to bad fields
formfield.addClass('form-control-danger');
label.addClass('has-danger');
msgbox.html(msg).addClass('has-danger').removeClass('hide');
// check if this field comes first in DOM
var domfield = formfield.get(0);
if (!firstfield || (firstfield && firstfield.compareDocumentPosition(domfield) === 2)) {
firstfield = domfield;
}
});
// focus on first bad field
firstfield.focus();
// scroll to above that field to make it visible
$('html, body').animate({
scrollTop: $(firstfield).offset().top - 200
}, 100);
}
});
});
// remove all error classes
function removeErrors() {
$(".form-control-danger").removeClass('form-control-danger');
$("[data-field]").removeClass('has-danger');
$(".form-control-feedback[data-field]").addClass('hide');
}
});
</script>
{% endblock %}

View file

@ -0,0 +1,146 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">
Item Types
</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-12">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__body">
<div class="m-form m-form--label-align-right m--margin-top-20 m--margin-bottom-30">
<div class="row align-items-center">
<div class="col-xl-8 order-2 order-xl-1">
<div class="form-group m-form__group row align-items-center">
<div class="col-md-4">
<div class="m-input-icon m-input-icon--left">
<input type="text" class="form-control m-input m-input--solid" placeholder="Search..." id="data-rows-search">
<span class="m-input-icon__icon m-input-icon__icon--left">
<span><i class="la la-search"></i></span>
</span>
</div>
</div>
</div>
</div>
<div class="col-xl-4 order-1 order-xl-2 m--align-right">
<a href="{{ url('item_type_add_form') }}" class="btn btn-focus m-btn m-btn--custom m-btn--icon m-btn--air m-btn--pill">
<span>
<i class="la la-industry"></i>
<span>New Item Type</span>
</span>
</a>
<div class="m-separator m-separator--dashed d-xl-none"></div>
</div>
</div>
</div>
<!--begin: Datatable -->
<div id="data-rows"></div>
<!--end: Datatable -->
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
var options = {
data: {
type: 'remote',
source: {
read: {
url: '{{ url("item_type_rows") }}',
method: 'POST'
}
},
saveState: {
cookie: false,
webstorage: false
},
pageSize: 10,
serverPaging: true,
serverFiltering: true,
serverSorting: true
},
layout: {
scroll: true
},
columns: [
{
field: 'id',
title: 'ID',
width: 30
},
{
field: 'name',
title: 'Name'
},
{
field: 'Actions',
width: 110,
title: 'Actions',
sortable: false,
overflow: 'visible',
template: function (row, index, datatable) {
var actions = '';
if (row.meta.update_url != '') {
actions += '<a href="' + row.meta.update_url + '" class="m-portlet__nav-link btn m-btn m-btn--hover-accent m-btn--icon m-btn--icon-only m-btn--pill btn-edit" data-id="' + row.name + '" title="Edit"><i class="la la-edit"></i></a>';
}
if (row.meta.delete_url != '') {
actions += '<a href="' + row.meta.delete_url + '" class="m-portlet__nav-link btn m-btn m-btn--hover-danger m-btn--icon m-btn--icon-only m-btn--pill btn-delete" data-id="' + row.name + '" title="Delete"><i class="la la-trash"></i></a>';
}
return actions;
},
}
],
search: {
onEnter: false,
input: $('#data-rows-search'),
delay: 400
}
};
var table = $("#data-rows").mDatatable(options);
$(document).on('click', '.btn-delete', function(e) {
var url = $(this).prop('href');
var id = $(this).data('id');
var btn = $(this);
e.preventDefault();
swal({
title: 'Confirmation',
html: 'Are you sure you want to delete <strong>' + id + '</strong>?',
type: 'warning',
showCancelButton: true
}).then((result) => {
if (result.value) {
$.ajax({
method: "DELETE",
url: url
}).done(function(response) {
table.row(btn.parents('tr')).remove();
table.reload();
});
}
});
});
});
</script>
{% endblock %}

View file

@ -0,0 +1,177 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">Items</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-6">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__head">
<div class="m-portlet__head-caption">
<div class="m-portlet__head-title">
<span class="m-portlet__head-icon">
<i class="la la-industry"></i>
</span>
<h3 class="m-portlet__head-text">
{% if mode == 'update' %}
Edit Item
<small>{{ obj.getName() }}</small>
{% else %}
New Item
{% endif %}
</h3>
</div>
</div>
</div>
<form id="row-form" class="m-form m-form--fit m-form--label-align-right m-form--group-seperator-dashed" method="post" action="{{ mode == 'update' ? url('item_type_update_submit', {'id': obj.getId()}) : url('item_add_submit') }}">
<div class="m-portlet__body">
<div class="form-group m-form__group row no-border">
<label class="col-lg-3 col-form-label" data-field="item_type">
Item Type:
</label>
<div class="col-lg-9">
<select class="form-control m-input" id="item-type" name="item_type">
{% for id, label in sets.item_types %}
{% if obj.getItemType %}
<option value="{{ id }}"{{ obj.getItemType.getID == id ? ' selected' }}>{{ label }}</option>
{% else %}
<option value="{{ id }}">{{ label }}</option>
{% endif %}
{% endfor %}
</select>
<div class="form-control-feedback hide" data-field="item_type"></div>
</div>
</div>
<div class="form-group m-form__group row no-border
{% if obj.getItemType %}
{% if obj.getItemType.getCode is not same as ('battery') %}
hide
{% endif %}
{% else %}
hide
{% endif %}
" id="battery-row">
<label class="col-lg-3 col-form-label" data-field="battery">
Battery:
</label>
<div class="col-lg-9">
<select class="form-control m-input" id="item-type" name="battery">
{% for id, label in sets.batteries %}
<option value="{{ id }}"{{ obj.getItemID == id ? ' selected' }}>{{ label }}</option>
{% endfor %}
</select>
<div class="form-control-feedback hide" data-field="battery"></div>
</div>
</div>
</div>
<div class="m-portlet__foot m-portlet__foot--fit">
<div class="m-form__actions m-form__actions--solid m-form__actions--right">
<div class="row">
<div class="col-lg-12">
<button type="submit" class="btn btn-success">Submit</button>
<a href="{{ url('item_list') }}" class="btn btn-secondary">Back</a>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
$("#row-form").submit(function(e) {
var form = $(this);
e.preventDefault();
$.ajax({
method: "POST",
url: form.prop('action'),
data: form.serialize()
}).done(function(response) {
// remove all error classes
removeErrors();
swal({
title: 'Done!',
text: 'Your changes have been saved!',
type: 'success',
onClose: function() {
window.location.href = "{{ url('item_list') }}";
}
});
}).fail(function(response) {
if (response.status == 422) {
var errors = response.responseJSON.errors;
var firstfield = false;
// remove all error classes first
removeErrors();
// display errors contextually
$.each(errors, function(field, msg) {
var formfield = $("[name='" + field + "']");
var label = $("label[data-field='" + field + "']");
var msgbox = $(".form-control-feedback[data-field='" + field + "']");
// add error classes to bad fields
formfield.addClass('form-control-danger');
label.addClass('has-danger');
msgbox.html(msg).addClass('has-danger').removeClass('hide');
// check if this field comes first in DOM
var domfield = formfield.get(0);
if (!firstfield || (firstfield && firstfield.compareDocumentPosition(domfield) === 2)) {
firstfield = domfield;
}
});
// focus on first bad field
firstfield.focus();
// scroll to above that field to make it visible
$('html, body').animate({
scrollTop: $(firstfield).offset().top - 200
}, 100);
}
});
});
// remove all error classes
function removeErrors() {
$(".form-control-danger").removeClass('form-control-danger');
$("[data-field]").removeClass('has-danger');
$(".form-control-feedback[data-field]").addClass('hide');
}
});
$('#item-type').change(function(e) {
console.log('item type change ' + e.target.value);
if (e.target.value === '1') {
// display battery row
$('#battery-row').removeClass("hide");
// hide service offering rows
} else {
// display service offering row
// hide battery row
$('#battery-row').addClass("hide");
}
})
</script>
{% endblock %}

View file

@ -0,0 +1,146 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">
Items
</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-12">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__body">
<div class="m-form m-form--label-align-right m--margin-top-20 m--margin-bottom-30">
<div class="row align-items-center">
<div class="col-xl-8 order-2 order-xl-1">
<div class="form-group m-form__group row align-items-center">
<div class="col-md-4">
<div class="m-input-icon m-input-icon--left">
<input type="text" class="form-control m-input m-input--solid" placeholder="Search..." id="data-rows-search">
<span class="m-input-icon__icon m-input-icon__icon--left">
<span><i class="la la-search"></i></span>
</span>
</div>
</div>
</div>
</div>
<div class="col-xl-4 order-1 order-xl-2 m--align-right">
<a href="{{ url('item_add_form') }}" class="btn btn-focus m-btn m-btn--custom m-btn--icon m-btn--air m-btn--pill">
<span>
<i class="la la-industry"></i>
<span>New Item</span>
</span>
</a>
<div class="m-separator m-separator--dashed d-xl-none"></div>
</div>
</div>
</div>
<!--begin: Datatable -->
<div id="data-rows"></div>
<!--end: Datatable -->
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
var options = {
data: {
type: 'remote',
source: {
read: {
url: '{{ url("item_rows") }}',
method: 'POST'
}
},
saveState: {
cookie: false,
webstorage: false
},
pageSize: 10,
serverPaging: true,
serverFiltering: true,
serverSorting: true
},
layout: {
scroll: true
},
columns: [
{
field: 'id',
title: 'ID',
width: 30
},
{
field: 'name',
title: 'Name'
},
{
field: 'Actions',
width: 110,
title: 'Actions',
sortable: false,
overflow: 'visible',
template: function (row, index, datatable) {
var actions = '';
if (row.meta.update_url != '') {
actions += '<a href="' + row.meta.update_url + '" class="m-portlet__nav-link btn m-btn m-btn--hover-accent m-btn--icon m-btn--icon-only m-btn--pill btn-edit" data-id="' + row.name + '" title="Edit"><i class="la la-edit"></i></a>';
}
if (row.meta.delete_url != '') {
actions += '<a href="' + row.meta.delete_url + '" class="m-portlet__nav-link btn m-btn m-btn--hover-danger m-btn--icon m-btn--icon-only m-btn--pill btn-delete" data-id="' + row.name + '" title="Delete"><i class="la la-trash"></i></a>';
}
return actions;
},
}
],
search: {
onEnter: false,
input: $('#data-rows-search'),
delay: 400
}
};
var table = $("#data-rows").mDatatable(options);
$(document).on('click', '.btn-delete', function(e) {
var url = $(this).prop('href');
var id = $(this).data('id');
var btn = $(this);
e.preventDefault();
swal({
title: 'Confirmation',
html: 'Are you sure you want to delete <strong>' + id + '</strong>?',
type: 'warning',
showCancelButton: true
}).then((result) => {
if (result.value) {
$.ajax({
method: "DELETE",
url: url
}).done(function(response) {
table.row(btn.parents('tr')).remove();
table.reload();
});
}
});
});
});
</script>
{% endblock %}

View file

@ -636,7 +636,11 @@
<select class="form-control m-input" id="invoice-promo" name="invoice_promo">
<option value="">None</option>
{% for promo in promos %}
{% if obj.getInvoice and obj.getInvoice.getPromo %}
<option value="{{ promo.getID() }}" {{ obj.getInvoice.getPromo.getID == promo.getID ? ' selected'}}>{{ promo.getName() ~ ' (' ~ promo.getDiscountRate * 100 ~ '% applied to ' ~ discount_apply[promo.getDiscountApply] ~ ')' }}</option>
{% else %}
<option value="{{ promo.getID() }}">{{ promo.getName() ~ ' (' ~ promo.getDiscountRate * 100 ~ '% applied to ' ~ discount_apply[promo.getDiscountApply] ~ ')' }}</option>
{% endif %}
{% endfor %}
</select>
<div class="form-control-feedback hide" data-field="invoice_promo"></div>
@ -827,20 +831,28 @@
<div class="m-form__section">
<div class="form-group m-form__group row">
<div class="col-lg-2">
<label for="faciliateted-type">Battery Facilitated By</label>
<label for="facilitated-type">Battery Facilitated By</label>
<select name="facilitated_type" class="form-control m-input" id="facilitated-type">
<option value="">None</option>
{% for key, type in facilitated_types %}
{% if obj.getFacilitatedType %}
<option value="{{ key }}"{{ obj.getFacilitatedType == key ? ' selected' }}>{{ type }}</option>
{% else %}
<option value="{{ key }}">{{ type }}</option>
{% endif %}
{% endfor %}
</select>
</div>
<div class="col-lg-8">
<label for="faciliateted-by">&nbsp;</label>
<label for="facilitated-by">&nbsp;</label>
<select name="facilitated_by" class="form-control m-input" id="facilitated-by">
<option value="">None</option>
{% for key, name in facilitated_hubs %}
{% if obj.getFacilitatedBy %}
<option value="{{ key }}"{{ obj.getFacilitatedBy.getID == key ? ' selected' }}>{{ name }}</option>
{% else %}
<option value="{{ key }}">{{ name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
@ -1194,6 +1206,10 @@
<script src="/assets/vendors/custom/gmaps/gmaps.js" type="text/javascript"></script>
<script>
var invoiceItems = [];
var hasCoolant = 0;
var hasSealant = 0;
// location search autocomplete
var input = document.getElementById('m_gmap_address');
@ -1218,6 +1234,7 @@ autocomplete.addListener('place_changed', function() {
$(function() {
var form_in_process = false;
var invoiceItems = [];
// openstreet maps stuff
// TODO: move this to a service
@ -1228,9 +1245,54 @@ $(function() {
var markerLayerGroup = L.layerGroup().addTo(osm_map);
function populateInvoiceItems()
{
{% if invoice_items is defined %}
{% for item in invoice_items %}
var qty = {{ item.getQuantity }};
{% if item.getBattery is not null %}
var battery_id = {{ item.getBattery.getID }};
invoiceItems.push({
battery: battery_id,
quantity: qty,
trade_in: '',
});
{% else %}
{% if item.getBatterySize is not null %}
var battery_size = {{ item.getBatterySize.GetID }};
var trade_in_type = '{{ item.getTradeInType }}';
// add to invoice array
invoiceItems.push({
battery_size: battery_size,
quantity: qty,
trade_in: trade_in_type,
});
{% endif %}
{% endif %}
{% endfor %}
// need to check if jo has coolant or sealant
{% if obj.getServiceType == 'overheat' %}
{% if obj.hasCoolant == 1 %}
hasCoolant = 1;
{% endif %}
{% endif %}
{% if obj.getServiceType == 'tire' %}
{% if obj.hasSealant == 1 %}
hasSealant = 1;
{% endif %}
{% endif %}
{% endif %}
}
function selectPoint(lat, lng)
{
// check if point is in coverage area
// commenting out the geofence call for CRM
/*
$.ajax({
method: "GET",
url: "{{ url('jo_geofence') }}",
@ -1248,7 +1310,7 @@ $(function() {
type: 'warning',
});
}
});
}); */
// clear markers
markerLayerGroup.clearLayers();
@ -1264,6 +1326,9 @@ $(function() {
$('#map_lat').val(lat);
$('#map_lng').val(lng);
// regenerate invoice
generateInvoice();
}
osm_map.on('click', function(e) {
@ -1307,6 +1372,11 @@ $(function() {
// OSM code
var lat = {{ obj.getCoordinates.getLatitude }};
var lng = {{ obj.getCoordinates.getLongitude }};
var promo = $("#invoice-promo").val();
populateInvoiceItems();
selectPoint(lat, lng);
// remove placeholder text
@ -1690,8 +1760,6 @@ $(function() {
placeholder: ""
});
var invoiceItems = [];
{% include 'invoice/trade_in.js.twig' %}
// add to invoice
@ -1761,6 +1829,8 @@ $(function() {
var table = $("#invoice-table tbody");
var stype = $("#service_type").val();
var cvid = $("#customer-vehicle").val();
var lng = $("#map_lng").val();
var lat = $("#map_lat").val();
console.log(JSON.stringify(invoiceItems));
@ -1772,7 +1842,11 @@ $(function() {
'stype': stype,
'items': invoiceItems,
'promo': promo,
'cvid': cvid
'cvid': cvid,
'coord_lng': lng,
'coord_lat': lat,
'flag_coolant': hasCoolant,
'flag_sealant': hasSealant,
}
}).done(function(response) {
// mark as invoice changed

View file

@ -635,6 +635,8 @@ $(function() {
function selectPoint(lat, lng)
{
// check if point is in coverage area
// commenting out the geofence call for CRM
/*
$.ajax({
method: "GET",
url: "{{ url('jo_geofence') }}",
@ -652,7 +654,7 @@ $(function() {
type: 'warning',
});
}
});
}); */
// clear markers
markerLayerGroup.clearLayers();

View file

@ -0,0 +1,154 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">Price Tiers</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-6">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__head">
<div class="m-portlet__head-caption">
<div class="m-portlet__head-title">
<span class="m-portlet__head-icon">
<i class="la la-industry"></i>
</span>
<h3 class="m-portlet__head-text">
{% if mode == 'update' %}
Edit Price Tier
<small>{{ obj.getName() }}</small>
{% else %}
New Price Tier
{% endif %}
</h3>
</div>
</div>
</div>
<form id="row-form" class="m-form m-form--fit m-form--label-align-right m-form--group-seperator-dashed" method="post" action="{{ mode == 'update' ? url('price_tier_update_submit', {'id': obj.getId()}) : url('price_tier_add_submit') }}">
<div class="m-portlet__body">
<div class="form-group m-form__group row no-border">
<label class="col-lg-3 col-form-label" data-field="name">
Name:
</label>
<div class="col-lg-9">
<input type="text" name="name" class="form-control m-input" value="{{ obj.getName() }}">
<div class="form-control-feedback hide" data-field="name"></div>
</div>
</div>
<div class="form-group m-form__group row no-border">
<label class="col-lg-3 col-form-label" data-field="areas">
Coverage Area:
</label>
<div class="col-lg-9">
{% if sets.areas is empty %}
No available supported areas.
{% else %}
<div class="m-checkbox-list">
{% for id, label in sets.areas %}
<label class="m-checkbox">
<input type="checkbox" name="areas[]" value="{{ id }}"{{ id in obj.getSupportedAreas() ? ' checked' : '' }}>
{{ label }}
<span></span>
</label>
{% endfor %}
</div>
{% endif %}
<div class="form-control-feedback hide" data-field="areas"></div>
</div>
</div>
</div>
<div class="m-portlet__foot m-portlet__foot--fit">
<div class="m-form__actions m-form__actions--solid m-form__actions--right">
<div class="row">
<div class="col-lg-12">
<button type="submit" class="btn btn-success">Submit</button>
<a href="{{ url('price_tier_list') }}" class="btn btn-secondary">Back</a>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
$("#row-form").submit(function(e) {
var form = $(this);
e.preventDefault();
$.ajax({
method: "POST",
url: form.prop('action'),
data: form.serialize()
}).done(function(response) {
// remove all error classes
removeErrors();
swal({
title: 'Done!',
text: 'Your changes have been saved!',
type: 'success',
onClose: function() {
window.location.href = "{{ url('price_tier_list') }}";
}
});
}).fail(function(response) {
if (response.status == 422) {
var errors = response.responseJSON.errors;
var firstfield = false;
// remove all error classes first
removeErrors();
// display errors contextually
$.each(errors, function(field, msg) {
var formfield = $("[name='" + field + "']");
var label = $("label[data-field='" + field + "']");
var msgbox = $(".form-control-feedback[data-field='" + field + "']");
// add error classes to bad fields
formfield.addClass('form-control-danger');
label.addClass('has-danger');
msgbox.html(msg).addClass('has-danger').removeClass('hide');
// check if this field comes first in DOM
var domfield = formfield.get(0);
if (!firstfield || (firstfield && firstfield.compareDocumentPosition(domfield) === 2)) {
firstfield = domfield;
}
});
// focus on first bad field
firstfield.focus();
// scroll to above that field to make it visible
$('html, body').animate({
scrollTop: $(firstfield).offset().top - 200
}, 100);
}
});
});
// remove all error classes
function removeErrors() {
$(".form-control-danger").removeClass('form-control-danger');
$("[data-field]").removeClass('has-danger');
$(".form-control-feedback[data-field]").addClass('hide');
}
});
</script>
{% endblock %}

View file

@ -0,0 +1,146 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">
Price Tiers
</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-12">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__body">
<div class="m-form m-form--label-align-right m--margin-top-20 m--margin-bottom-30">
<div class="row align-items-center">
<div class="col-xl-8 order-2 order-xl-1">
<div class="form-group m-form__group row align-items-center">
<div class="col-md-4">
<div class="m-input-icon m-input-icon--left">
<input type="text" class="form-control m-input m-input--solid" placeholder="Search..." id="data-rows-search">
<span class="m-input-icon__icon m-input-icon__icon--left">
<span><i class="la la-search"></i></span>
</span>
</div>
</div>
</div>
</div>
<div class="col-xl-4 order-1 order-xl-2 m--align-right">
<a href="{{ url('price_tier_add_form') }}" class="btn btn-focus m-btn m-btn--custom m-btn--icon m-btn--air m-btn--pill">
<span>
<i class="la la-industry"></i>
<span>New Price Tier</span>
</span>
</a>
<div class="m-separator m-separator--dashed d-xl-none"></div>
</div>
</div>
</div>
<!--begin: Datatable -->
<div id="data-rows"></div>
<!--end: Datatable -->
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
var options = {
data: {
type: 'remote',
source: {
read: {
url: '{{ url("price_tier_rows") }}',
method: 'POST'
}
},
saveState: {
cookie: false,
webstorage: false
},
pageSize: 10,
serverPaging: true,
serverFiltering: true,
serverSorting: true
},
layout: {
scroll: true
},
columns: [
{
field: 'id',
title: 'ID',
width: 30
},
{
field: 'name',
title: 'Name'
},
{
field: 'Actions',
width: 110,
title: 'Actions',
sortable: false,
overflow: 'visible',
template: function (row, index, datatable) {
var actions = '';
if (row.meta.update_url != '') {
actions += '<a href="' + row.meta.update_url + '" class="m-portlet__nav-link btn m-btn m-btn--hover-accent m-btn--icon m-btn--icon-only m-btn--pill btn-edit" data-id="' + row.name + '" title="Edit"><i class="la la-edit"></i></a>';
}
if (row.meta.delete_url != '') {
actions += '<a href="' + row.meta.delete_url + '" class="m-portlet__nav-link btn m-btn m-btn--hover-danger m-btn--icon m-btn--icon-only m-btn--pill btn-delete" data-id="' + row.name + '" title="Delete"><i class="la la-trash"></i></a>';
}
return actions;
},
}
],
search: {
onEnter: false,
input: $('#data-rows-search'),
delay: 400
}
};
var table = $("#data-rows").mDatatable(options);
$(document).on('click', '.btn-delete', function(e) {
var url = $(this).prop('href');
var id = $(this).data('id');
var btn = $(this);
e.preventDefault();
swal({
title: 'Confirmation',
html: 'Are you sure you want to delete <strong>' + id + '</strong>?',
type: 'warning',
showCancelButton: true
}).then((result) => {
if (result.value) {
$.ajax({
method: "DELETE",
url: url
}).done(function(response) {
table.row(btn.parents('tr')).remove();
table.reload();
});
}
});
});
});
</script>
{% endblock %}

View file

@ -13,7 +13,8 @@ add_cust_vehicle_battery_info: This vehicle is using a Motolite battery
jo_title_pdf: Motolite Res-Q Job Order
country_code_prefix: '+63'
delivery_instructions_label: Delivery Instructions
no_inventory_message: No stock for [item_display]
no_inventory_message: 'A Job Order was created but there is insufficient stock for the following SKU(s) on this branch: [item_display]'
no_riders_message: A Job Order was created but there are no riders available for this branch.
# images
image_logo_login: /assets/images/logo-resq.png
@ -159,6 +160,7 @@ menu.database.subtickettypes: 'Sub Ticket Types'
menu.database.emergencytypes: 'Emergency Types'
menu.database.ownershiptypes: 'Ownership Types'
menu.database.serviceofferings: 'Service Offerings'
menu.database.itemtypes: 'Item Types'
# fcm jo status updates
jo_fcm_title_outlet_assign: 'Looking for riders'

View file

@ -0,0 +1,2 @@
update supported_area set hub_filter_exceptions = '{"no_inventory":true,"no_available_rider":true}' where id = 34;
update supported_area set hub_filter_exceptions = '{"no_inventory":true,"no_available_rider":true}' where id = 35;

View file

@ -0,0 +1,53 @@
-- MySQL dump 10.19 Distrib 10.3.39-MariaDB, for Linux (x86_64)
--
-- Host: localhost Database: resq
-- ------------------------------------------------------
-- Server version 10.3.39-MariaDB
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `item_type`
--
DROP TABLE IF EXISTS `item_type`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `item_type` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(80) NOT NULL,
`code` varchar(80) NOT NULL,
PRIMARY KEY (`id`),
KEY `item_type_idx` (`code`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `item_type`
--
LOCK TABLES `item_type` WRITE;
/*!40000 ALTER TABLE `item_type` DISABLE KEYS */;
INSERT INTO `item_type` VALUES (1,'Battery','battery'),(2,'Service Offering','service_offering');
/*!40000 ALTER TABLE `item_type` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2024-01-28 20:59:44

View file

@ -0,0 +1 @@
INSERT INTO service_offering (name, code, fee) VALUES ('Motolite User Jumpstart Warranty Fee', 'motolite_user_jumpstart_warranty_fee', 200.00);

View file

@ -0,0 +1 @@
INSERT INTO service_offering (name, code, fee) VALUES ('Tire Sealant Fee', 'tire_sealant_fee', '200.00');

View file

@ -0,0 +1,54 @@
-- MySQL dump 10.19 Distrib 10.3.39-MariaDB, for Linux (x86_64)
--
-- Host: localhost Database: resq
-- ------------------------------------------------------
-- Server version 10.3.39-MariaDB
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `service_offering`
--
DROP TABLE IF EXISTS `service_offering`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `service_offering` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(80) NOT NULL,
`code` varchar(80) NOT NULL,
`fee` decimal(9,2) NOT NULL,
PRIMARY KEY (`id`),
KEY `service_offering_idx` (`code`)
) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `service_offering`
--
LOCK TABLES `service_offering` WRITE;
/*!40000 ALTER TABLE `service_offering` DISABLE KEYS */;
INSERT INTO `service_offering` VALUES (1,'Tax','tax',0.12),(2,'Motolite User Service Fee','motolite_user_service_fee',0.00),(3,'Battery Replacement Warranty Fee','battery_replacement_warranty_fee',0.00),(4,'Fuel Service Fee','fuel_service_fee',300.00),(5,'Fuel Gas Fee','fuel_gas_fee',340.00),(6,'Fuel Diesel Fee','fuel_diesel_fee',320.00),(7,'Jumpstart Fee','jumpstart_fee',300.00),(8,'Jumpstart Fee Mobile App','jumpstart_fee_mobile_app',300.00),(9,'Jumpstart Warranty Fee','jumpstart_warranty_fee',300.00),(10,'Overheat Fee','overheat_fee',300.00),(11,'Coolant Fee','coolant_fee',1600.00),(12,'Post Recharged Fee','post_recharged_fee',300.00),(13,'Post Replacement Fee','post_replacement_fee',0.00),(14,'Tire Repair Fee','tire_repair_fee',300.00),(17,'Motolite User Jumpstart Warranty Fee','motolite_user_jumpstart_warranty_fee',200.00),(18,'Motolite User Jumpstart Fee','motolite_user_jumpstart_fee',200.00);
/*!40000 ALTER TABLE `service_offering` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2024-04-02 1:17:40