├── .ackrc
├── .editorconfig
├── .gitignore
├── LICENSE
├── README.md
├── boot.php
├── composer.json
├── composer.lock
├── doc
└── index.ad
├── etc
├── Doxyfile
├── apache2-example.conf
├── config-example.php
├── receipt-foot.txt
├── receipt-head.txt
├── receipt-tail.txt
└── uom.ini
├── feature
└── POS.feature
├── lib
├── B2C
│ ├── Sale.php
│ └── Sale
│ │ └── Item.php
├── Cart.php
├── Controller
│ ├── API
│ │ ├── B2C.php
│ │ ├── B2C
│ │ │ ├── Item.php
│ │ │ ├── Payment.php
│ │ │ ├── Receipt.php
│ │ │ └── Single.php
│ │ ├── Base.php
│ │ └── Main.php
│ ├── Auth
│ │ ├── Init.php
│ │ ├── Shut.php
│ │ └── oAuth2
│ │ │ ├── Back.php
│ │ │ └── Open.php
│ ├── CRM
│ │ ├── Contact.php
│ │ ├── Main.php
│ │ └── Message.php
│ ├── Contact.php
│ ├── Dashboard
│ │ └── Main.php
│ ├── Intent.php
│ ├── Menu
│ │ └── Main.php
│ ├── POS
│ │ ├── Ajax.php
│ │ ├── Cart
│ │ │ ├── Ajax.php
│ │ │ ├── Drop.php
│ │ │ ├── Open.php
│ │ │ └── Save.php
│ │ ├── Checkout
│ │ │ ├── Commit.php
│ │ │ ├── Done.php
│ │ │ ├── Open.php
│ │ │ └── Receipt.php
│ │ ├── Delivery.php
│ │ ├── Main.php
│ │ ├── Online.php
│ │ └── Shut.php
│ ├── Report
│ │ ├── Ajax.php
│ │ └── Main.php
│ └── Shop
│ │ ├── Base.php
│ │ ├── Cart.php
│ │ ├── Checkout.php
│ │ ├── Example.php
│ │ └── Main.php
├── Inventory.php
├── Middleware
│ └── Auth.php
├── Module
│ ├── API.php
│ ├── B2B.php
│ ├── CRM.php
│ ├── Dashboard.php
│ ├── Menu.php
│ ├── POS.php
│ ├── Report.php
│ ├── Shop.php
│ └── Webhook.php
├── PDF
│ ├── Base.php
│ └── Receipt.php
├── Product.php
├── Product
│ └── Type.php
├── Service
│ └── Weedmaps.php
└── Traits
│ └── OpenAuthBox.php
├── make.sh
├── package-lock.json
├── package.json
├── sass
├── _base.scss
├── _pos-terminal.scss
└── main.scss
├── test
├── Base.php
├── Core
│ ├── Config_Test.php
│ ├── Core_Test.php
│ └── System_Test.php
├── Unit
│ └── API_Test.php
├── Webhook
│ └── Weedmaps_Test.php
├── phpunit.xml.dist
└── test.php
├── view
├── _block
│ ├── body-head.php
│ ├── inventory-list.php
│ ├── modal.php
│ ├── modal
│ │ └── pos
│ │ │ ├── card-swipe.php
│ │ │ ├── cart-options.php
│ │ │ ├── customer-info.php
│ │ │ ├── discount.php
│ │ │ ├── hold.php
│ │ │ ├── keypad.php
│ │ │ ├── loyalty.php
│ │ │ ├── payment-card.php
│ │ │ ├── payment-cash.php
│ │ │ ├── scan-id.php
│ │ │ └── transaction-limit.php
│ └── session-flash.php
├── _layout
│ ├── html-pos.php
│ ├── html.php
│ └── shop-html.php
├── contact
│ └── update.php
├── crm
│ ├── contact.php
│ ├── main.php
│ ├── message-compose-email.php
│ ├── message-compose-sms.php
│ └── message.php
├── dashboard.php
├── done.php
├── intent
│ ├── delivery-auth.php
│ ├── main.php
│ └── vendor-view.php
├── pos
│ ├── checkout
│ │ ├── done.php
│ │ └── receipt.php
│ ├── contact-select.php
│ ├── contact-verify.php
│ ├── delivery.php
│ ├── main.php
│ ├── online.php
│ ├── open.php
│ └── terminal
│ │ ├── main.php
│ │ └── shut.php
├── report
│ └── recent.php
└── shop
│ ├── cart.php
│ ├── checkout-done.php
│ ├── checkout.php
│ ├── example.php
│ └── main.php
└── webroot
├── .well-known
└── openthc
│ ├── app
│ └── pos
├── css
└── shop.css
├── index.html
├── js
├── pos-camera.js
├── pos-cart.js
├── pos-modal-cart-options.js
├── pos-modal-discount.js
├── pos-modal-loyalty.js
├── pos-modal-payment.js
├── pos-printer.js
├── pos-scanner.js
└── pos.js
├── loading.html
├── main.php
└── robots.txt
/.ackrc:
--------------------------------------------------------------------------------
1 | #
2 | # OpenTHC .ackrc
3 | #
4 |
5 | --ignore-dir=.bundle/
6 | --ignore-dir=.cache/
7 | --ignore-dir=.composer/
8 | --ignore-dir=.git/
9 | --ignore-dir=.idea/
10 | --ignore-dir=.local/
11 | --ignore-dir=.npm/
12 | --ignore-dir=.vscode/
13 | --ignore-dir=doxygen/
14 | --ignore-dir=log-archive/
15 | --ignore-dir=node_modules/
16 | --ignore-dir=output-data/
17 | --ignore-dir=public/
18 | --ignore-dir=source-data/
19 | --ignore-dir=tmp/
20 | --ignore-dir=var/
21 | --ignore-dir=vendor/
22 | --ignore-dir=webroot/assets/
23 | --ignore-dir=webroot/css/
24 | --ignore-dir=webroot/doc/
25 | --ignore-dir=webroot/img/
26 | --ignore-dir=webroot/lib/
27 | --ignore-dir=webroot/openapi-ui/
28 | --ignore-dir=webroot/output/
29 | --ignore-dir=webroot/pub/
30 | --ignore-dir=webroot/redoc/
31 | --ignore-dir=webroot/sdk/
32 | --ignore-dir=webroot/test-output/
33 | --ignore-dir=webroot/vendor/
34 |
35 |
36 | --ignore-file=is:.phpunit.result.cache
37 | --ignore-file=is:composer.lock
38 | --ignore-file=is:package-lock.json
39 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 4
7 | indent_style = tab
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | ;
12 | ; JSON files
13 | [*.json]
14 | indent_size = 4
15 | indent_style = tab
16 |
17 | ;
18 | ; YAML files
19 | [*.{yml,yaml}]
20 | indent_size = 2
21 | indent_style = space
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #
2 | # OpenTHC .gitignore
3 | #
4 |
5 | **/.idea/workspace.xml
6 | **/.idea/tasks.xml
7 | *.phar
8 | .build/
9 | .config/
10 | .env*
11 | .gradle/
12 | .idea/
13 | .local/
14 | .netlify/
15 | .vscode/
16 | bower_components/
17 | build/
18 | Custom/
19 | etc/
20 | node_modules/
21 | public/
22 | release/
23 | source-data/
24 | test/*.xsl
25 | test/phpunit.xml
26 | var/
27 | vendor/
28 | webroot/pub/
29 | webroot/test-output/
30 | webroot/vendor/
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cannabis Retail Software
2 |
3 | It's a pretty simple web-based point-of-sale, customer loyalty, online-menu system.
4 | IT can be used stand-alone or integrated with BioTrack, CCRS or Metrc.
5 |
6 | ## Installation
7 |
8 | git clone {this repo}
9 | ./make.sh install
10 | [ update apache config ]
11 | certbot ...
12 | [ update apache config for ssl ]
13 |
14 | Once the system is installed you'll have to configure it for your environment.
15 | That is, connect to the government CRE if you have one - configure license information, etc.
16 |
17 |
18 | ## Features
19 |
20 | * Point of Sale (POS)
21 | * Instore Signage (ChromeCast, Tablet)
22 | * Online Ordering, In-Store Kiosk
23 | * Customer Loyalty Program (CRM)
24 |
25 |
26 | ## Point of Sale
27 |
28 | * Auto-Print Pick Ticket
29 | * Save/Transfer Sale in Progress (Cart)
30 | * Customizable Receipt
31 | * Android and iOS Compatible (coming soon)
32 |
33 |
34 | ## Loyalty Program
35 |
36 | * SMS Campaigns
37 | * Email Loyalty Campaign
38 | * Participant Facets: Revenue, Specials, Concentrate, Edible, Flower, etc
39 |
40 |
41 | ## Vendor Management System
42 |
43 | Allows a Retail business to create their own Vendor Portal
44 |
45 | Vendor can visit and see when their Sample Day is / could be, sees instructions, request a time to stop by.
46 | Vendor can be rejected with a simple button press.
47 |
48 |
49 | ## Vendor Database
50 |
51 | Just supplemental data on the main dir.openthc.com
52 |
53 |
54 | ## Relationships
55 |
56 | Calendar, Terms, Vendor Notes
57 |
58 |
59 | ## Sample Processing
60 |
61 | Sample Calendar, Vendors Feedback
62 |
63 |
64 | ## Joint Inventory
65 |
66 | Shared Inventory between Retailer and Supplier
67 |
68 |
69 | ## Alternatives
70 |
71 | In case free & open-source software is not to your taste,
72 | or maybe you're just looking for more information,
73 | here are other offerings.
74 |
75 |
76 | ### Point of Sale
77 |
78 | * BioTrackTHC - https://www.biotrack.com/
79 | * Cova - https://www.covasoftware.com/pos
80 | * GreenBits - https://www.greenbits.com/
81 | * KlickTrack - https://getklicktrack.io
82 |
83 |
84 | ### Loyalty Offering
85 |
86 | * 420 Solutions - https://www.4twentysolutions.com/
87 | * CannaReward - http://www.cannareward.com/
88 | * SpringBig - https://www.springbig.com/
89 | * SproutCannabis - http://www.sprout.online
90 | * TokeIn - https://tokein.com/
91 |
92 |
93 | ### Online Ordering
94 |
95 | * Eaze - https://www.eaze.com/
96 | * WoahStork - https://www.woahstork.com/
97 |
98 |
99 | ### Hardware
100 |
101 | * [ELO Touch](https://www.elotouch.com/pos-terminals-paypoint-plus-for-windows.html)
102 | * [Heckler](https://hecklerdesign.com/)
103 |
--------------------------------------------------------------------------------
/boot.php:
--------------------------------------------------------------------------------
1 | Invalid Application Configuration [POS-017]', 500);
22 | }
23 |
24 | define('OPENTHC_SERVICE_ID', \OpenTHC\Config::get('openthc/pos/id'));
25 | define('OPENTHC_SERVICE_ORIGIN', \OpenTHC\Config::get('openthc/pos/origin'));
26 |
27 | _error_handler_init();
28 |
29 | /**
30 | * Database Connection
31 | */
32 | function _dbc($dsn=null)
33 | {
34 | static $dbc_list = [];
35 |
36 | if (empty($dsn)) {
37 | $dsn = 'auth';
38 | }
39 |
40 | $dbc = $dbc_list[$dsn];
41 | if (!empty($dbc)) {
42 | return $dbc;
43 | }
44 |
45 | switch ($dsn) {
46 | case 'auth':
47 | case 'main':
48 | case 'root':
49 |
50 | $cfg = \OpenTHC\Config::get(sprintf('database/%s', $dsn));
51 | if (empty($cfg)) {
52 | _exit_text('Invalid Database Configuration [ABD-039]', 500);
53 | }
54 |
55 | $dbc_list[$dsn] = new \Edoceo\Radix\DB\SQL(sprintf('pgsql:application_name=openthc-pos;host=%s;dbname=%s', $cfg['hostname'], $cfg['database']), $cfg['username'], $cfg['password']);
56 |
57 | return $dbc_list[$dsn];
58 |
59 | default:
60 |
61 | $dbc_list[$dsn] = new \Edoceo\Radix\DB\SQL($dsn);
62 |
63 | return $dbc_list[$dsn];
64 |
65 | }
66 |
67 | return null;
68 | }
69 |
70 | function _draw_html_card($head, $body, $foot=null)
71 | {
72 | ob_start();
73 | echo '
';
74 | printf('', $head);
75 | printf('
%s
', $body);
76 | if ($foot) {
77 | printf('', $foot);
78 | }
79 | echo '
';
80 |
81 | return ob_get_clean();
82 | }
83 |
84 | /**
85 | * You can put custom stuff here, it will be available to the entire application
86 | */
87 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openthc/pos",
3 | "description": "Cannabis Retail Point of Sale",
4 | "license": "GPL-3.0-only",
5 | "version": "420.24.326",
6 | "autoload": {
7 | "exclude-from-classmap": [
8 | "vendor/erusev/parsedown-extra/test"
9 | ],
10 | "psr-4": {
11 | "OpenTHC\\POS\\": "./lib",
12 | "OpenTHC\\POS\\Test\\": "./test"
13 | }
14 | },
15 | "require": {
16 | "openthc/common": "dev-main",
17 | "openthc/cre-adapter": "dev-main",
18 | "pear/net_smtp": "1.10.0",
19 | "tuupola/base32": "2.0.0",
20 | "tecnickcom/tcpdf": "6.9.0"
21 | },
22 | "require-dev": {
23 | "php-webdriver/webdriver": "1.15.2",
24 | "phpmd/phpmd": "2.15.0",
25 | "phpstan/phpstan": "2.1.11",
26 | "phpunit/phpunit": "9.6.22"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/doc/index.ad:
--------------------------------------------------------------------------------
1 | :toc: left
2 | :doctype: book
3 | :source-highlighter: pygments
4 | :pygments-style: rainbow_dash
5 | :icons: font
6 | :revealjsdir: https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.8.0
7 |
8 |
9 | = OpenTHC POS
10 |
11 | The OpenTHC POS Package is a web-based point of sale integrated with BioTrack, LeafData and METRC.
12 |
13 | == Installation
14 |
15 | Clone the repo onto your web-platform.
16 | This can be a remote host, or a computer running on-site, it has been known to operate from a Raspberry Pi.
17 |
18 | Connect the system to a database, a schema is not included here, one is available from the CRE package.
19 |
20 | Update the configuration to connect to the proper CRE.
21 |
22 | Process sales!
23 |
24 |
25 | == Overview
26 |
27 | === Retail Management System
28 |
29 | * Point-of-Sale/Terminal
30 | * Compliance Reporting Engine Integration (BT, LD, M)
31 | * Product Intake
32 | * ID Verification
33 | * Sales Limits
34 | * Discounts
35 | * Customer Checkin
36 | * Loyalty Programs
37 | * Online Ordering and Menus
38 |
39 |
40 | == Requirements
41 |
42 | Runs on Android tables, Apple iPad, Desktop.
43 | Linux and Windows.
44 |
45 | Handheld Scanner
46 | - Any
47 |
--------------------------------------------------------------------------------
/etc/apache2-example.conf:
--------------------------------------------------------------------------------
1 | #
2 | # pos.openthc
3 | #
4 |
5 | Define "pos_host" "pos.openthc.example.com"
6 | Define "pos_root" "/opt/openthc/pos"
7 |
8 |
9 | # webroot
10 |
11 |
12 |
13 | Require all denied
14 |
15 |
16 | AllowOverride None
17 | Options FollowSymLinks Indexes
18 | Require all granted
19 |
20 | # Headers
21 | Header set referrer-policy "same-origin"
22 | Header set x-content-type-options "nosniff"
23 | Header set x-frame-options "sameorigin"
24 | Header set x-xss-protection "1; mode=block"
25 |
26 | # Main Controller
27 | RewriteEngine On
28 | RewriteCond %{REQUEST_FILENAME} !-d
29 | RewriteCond %{REQUEST_FILENAME} !-f
30 | RewriteRule .* /main.php [L,QSA]
31 |
32 | # PHP Settings
33 | php_flag allow_url_fopen off
34 | php_flag allow_url_include off
35 | php_flag define_syslog_variables on
36 | php_flag display_errors off
37 | php_flag display_startup_errors off
38 | php_flag enable_dl off
39 | php_flag error_log on
40 | php_flag expose_php off
41 | php_flag html_errors off
42 | php_flag ignore_repeated_errors on
43 | php_flag ignore_repeated_source on
44 | php_flag implicit_flush off
45 | php_flag log_errors on
46 | php_flag magic_quotes_runtime off
47 | php_flag mail.add_x_header off
48 |
49 | php_value date.timezone UTC
50 | php_value error_reporting -1
51 | php_value max_execution_time 300
52 | php_value max_input_vars 1024
53 | php_value memory_limit 256M
54 | php_value post_max_size 48M
55 | php_value upload_max_filesize 32M
56 |
57 | # Session Data
58 | php_flag session.auto_start off
59 | php_flag session.cookie_httponly on
60 | php_flag session.cookie_secure on
61 | php_flag session.use_strict_mode on
62 | php_value session.cookie_lifetime 14400
63 | php_value session.cookie_samesite lax
64 | php_value session.gc_maxlifetime 3600
65 | php_value session.name openthc
66 |
67 |
68 |
69 |
70 | #
71 | # HTTP
72 |
73 |
74 | ServerName ${pos_host}
75 | DocumentRoot ${pos_root}/webroot
76 |
77 | RewriteEngine On
78 | RewriteCond %{HTTPS} !=on
79 | RewriteRule ^/.well-known - [END]
80 | RewriteRule .* https://%{SERVER_NAME}%{REQUEST_URI} [R=301,L]
81 |
82 |
83 |
84 |
85 | #
86 | # HTTPS
87 |
88 |
89 | ServerName ${pos_host}
90 | DocumentRoot ${pos_root}/webroot
91 |
92 | SSLEngine On
93 | SSLCertificateFile /etc/letsencrypt/live/${pos_host}/fullchain.pem
94 | SSLCertificateKeyFile /etc/letsencrypt/live/${pos_host}/privkey.pem
95 |
96 | # Authorization Header
97 | RewriteEngine On
98 | RewriteCond %{HTTP:Authorization} .
99 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
100 |
101 |
102 |
--------------------------------------------------------------------------------
/etc/config-example.php:
--------------------------------------------------------------------------------
1 | [
11 | 'hostname' => 'localhost',
12 | 'username' => 'openthc_auth',
13 | 'password' => 'openthc_auth',
14 | 'database' => 'openthc_auth',
15 | ],
16 | 'main' => [
17 | 'hostname' => 'localhost',
18 | 'username' => 'openthc_main',
19 | 'password' => 'openthc_main',
20 | 'database' => 'openthc_main',
21 | ]
22 | ];
23 |
24 | // Redis
25 | $cfg['redis'] = [
26 | 'hostname' => '127.0.0.1',
27 | ];
28 |
29 | // Statsd
30 | $cfg['statsd'] = [
31 | 'hostname' => '127.0.0.1',
32 | ];
33 |
34 | // OpenTHC Services
35 | $cfg['openthc'] = [
36 | 'app' => [
37 | 'origin' => 'https://app.openthc.example',
38 | 'public' => '/* APP SERVICE PUBLIC KEY */',
39 | ],
40 | 'b2b' => [
41 | 'origin' => 'https://b2b.openthc.example',
42 | 'public' => 'x',
43 | 'secret' => 'x',
44 | ],
45 | 'dir' => [
46 | 'origin' => 'https://dir.openthc.example',
47 | 'public' => '/* DIR SERVICE PUBLIC KEY */',
48 | ],
49 | 'pipe' => [
50 | 'origin' => 'https://pipe.openthc.example',
51 | 'public' => '/* PIPE SERVICE PUBLIC KEY */',
52 | ],
53 | 'pos' => [
54 | 'id' => '/* POS SERVICE ULID */',
55 | 'origin' => 'https://pos.openthc.example',
56 | 'public' => '/* POS SERVICE PUBLIC KEY */',
57 | 'secret' => '/* POS SERVICE SECRET KEY */',
58 | ],
59 | 'sso' => [
60 | 'origin' => 'https://sso.openthc.example',
61 | 'public' => '/* SSO SERVICE PUBLIC KEY */',
62 | 'client-id' => '/* POS SERVICE ULID */',
63 | 'client-sk' => '/* POS SERVICE SSO CLIENT SECRET KEY */'
64 | // 'scope' => 'contact company profile cre pos',
65 | ]
66 | ];
67 |
68 | // Google
69 | $cfg['google'] = [
70 | 'api_key_js' => '',
71 | 'map_api_key_js' => '',
72 | ];
73 |
74 | return $cfg;
75 |
--------------------------------------------------------------------------------
/etc/receipt-foot.txt:
--------------------------------------------------------------------------------
1 | *** EDIT etc/receipt-foot.txt ***
2 |
3 | ALL SALES FINAL
4 |
5 | EXCHANGES
6 | Exchanges for defective cartridges must be for similar item only.
7 | Exchanges must be made within 10 days of purchae to the same location.
8 | Exchanges must have oringal packaging and receipt.
9 | No Exceptions.
10 |
11 | WARNING: This product has intoxicating effects and may be habit forming.
12 | Cannabis can impair concentration, coordination and judgement.
13 | There may be health risks associated with consumption of this product.
14 | Should not be used by women that are pregnant or breast feeding.
15 | Smoking is hazardous to your health.
16 | Do not operate a vehicle or machinery under the influence of this drug.
17 | This product may be unlawful outside your jurisdiction.
18 | For use by adults over 21 years old.
19 | Pesticide information available upon request.
20 |
21 | KEEP OUT OF REACH OF CHILDREN
22 |
23 | *** EDIT etc/receipt-foot.txt ***
24 |
--------------------------------------------------------------------------------
/etc/receipt-head.txt:
--------------------------------------------------------------------------------
1 | *** EDIT RECEIPT HEAD ***
2 |
--------------------------------------------------------------------------------
/etc/receipt-tail.txt:
--------------------------------------------------------------------------------
1 | *** EDIT etc/receipt-tail.txt ***
2 |
3 | THANKS FOR SHOPPING AT $license_name
4 |
5 | 123 Fake St #4
6 | Big City, ST, 56789
7 | store@cannabis.example.com
8 | http://domain.example.com/
9 | +1 (555) 555-5555
10 |
11 | *** EDIT etc/receipt-tail.txt ***
12 |
--------------------------------------------------------------------------------
/etc/uom.ini:
--------------------------------------------------------------------------------
1 | ; Unit of Measure Data
2 | ; "name" is the Name in our System, it's key is the stub
3 |
4 | [ea]
5 | name = "Each"
6 | biotrack = true
7 | metrc = true
8 | metrc_mode = "Count"
9 |
10 | [g]
11 | name = "Grams"
12 | biotrack = true
13 | metrc = true
14 |
15 | [kg]
16 | name = "Kilograms"
17 | biotrack = true
18 | metrc = true
19 | metrc_mode = "Weight"
20 |
21 | [mg]
22 | name = "Milligrams"
23 | biotrack = true
24 | metrc = true
25 | metrc_mode = "Weight"
26 |
27 | [l]
28 | name = "Liters"
29 | biotrack = false
30 | metrc = false
31 | leafdata = false
32 |
33 | [lb]
34 | name = "Pounds"
35 | biotrack = true
36 | metrc = true
37 | metrc_mode = "Weight"
38 |
39 |
40 | ; BioTrack Supported
41 | ; METRC Not Supported
42 | [ml]
43 | name = "Milliliters"
44 | biotrack = true
45 | metrc = false
46 |
47 |
48 | [oz]
49 | name = "Ounces"
50 | biotrack = true
51 | metrc = true
52 | metrc_mode = "Weight"
53 |
--------------------------------------------------------------------------------
/feature/POS.feature:
--------------------------------------------------------------------------------
1 | #
2 | # This describes the behavior of the POS application and
3 | # the basic features of the point-of-sale interface
4 | #
5 |
6 | Feature: Point of Sale
7 |
8 | In order to release product from the custody of a retail licensee
9 | Retail Contacts should be able to sell products to customers
10 |
11 | Scenario: Open the POS application from the App Dashboard
12 | Given I am a Retail Contact
13 | When I click Launch POS
14 | Then I should see the POS Dashboard
15 |
16 | Scenario: Open the POS application's Point-Of-Sale interface
17 | Given I am a Retail Contact logged into POS
18 | When I click the POS icon
19 | And I enter "1234" as my Employee Code
20 | Then I should see the point-of-sale interface
21 |
22 | Scenario: Find all active Products
23 | Given I am a Retail Contact with the POS interface open
24 | When I click the Search icon
25 | Then all active Products display in the list categorized by Product Type
26 |
27 | Scenario: Add a Product to the active Ticket
28 | Given I am a Retail Contact with the POS interface open
29 | When I click on a Product
30 | Then it is added to the Product list in the active Ticket
31 |
32 | Scenario: Add the same Product to the active Ticket
33 | Given I am a Retail Contact with one Product in the active Ticket
34 | When I click on the same Product in the active Products list
35 | Then the quantity of that Product in the active Ticket is increased by 1
36 |
37 | Scenario: Open Payment interface for an active Ticket
38 | Given I am a Retail Contact with an active Ticket worth $10.95
39 | When I press the Payment button
40 | Then I can see the payment interface
41 |
42 | Scenario: Process payment for active Ticket
43 | Given I am a Retail Contact with an active Ticket worth $10.95
44 | When I press $20 in the payment interface
45 | Then I see the $9.05 change due
46 |
47 | Scenario: Complete the sale for the active Ticket
48 | Given I am a Retail Contact that has processed all change due
49 | When I press the Complete button
50 | Then I see the Recept interface
51 |
--------------------------------------------------------------------------------
/lib/B2C/Sale.php:
--------------------------------------------------------------------------------
1 | _dbc->fetchRow($sql, $arg);
27 | if (empty($res)) {
28 | $S = new Sale();
29 | $S['id'] = $id;
30 | $S['uid'] = $_SESSION['Contact']['id'];
31 | $S['license_id'] = $License['id'];
32 | $S->save();
33 | } else {
34 | $S = new Sale($res);
35 | }
36 |
37 | return $S;
38 |
39 | }
40 |
41 | function addItem($I, $q)
42 | {
43 | $this->_item_list[] = array(
44 | 'id' => $I['id'],
45 | 'size' => $q,
46 | );
47 |
48 | }
49 |
50 | function getItems()
51 | {
52 | $sql = 'SELECT * FROM b2c_sale_item WHERE b2c_sale_id = ? ORDER BY sort, id';
53 | $arg = array($this->_data['id']);
54 | $res = $this->_dbc->fetchAll($sql, $arg);
55 | return $res;
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/lib/B2C/Sale/Item.php:
--------------------------------------------------------------------------------
1 | rdb = $rdb;
34 |
35 | if (empty($key)) {
36 | $key = _ulid();
37 | }
38 |
39 | $this->id = $key;
40 | $this->key = sprintf('/%s/cart/%s', $_SESSION['License']['id'], $this->id);
41 | $this->item_list = new \stdClass();
42 |
43 | $chk = $this->rdb->get($this->key);
44 | if ( ! empty($chk)) {
45 | $chk = json_decode($chk);
46 | foreach ($chk as $k => $v) {
47 | $this->{$k} = $v;
48 | }
49 | }
50 |
51 | }
52 |
53 | function save()
54 | {
55 | $val = json_encode($this);
56 |
57 | // $val = [];
58 | // $val['id'] = $this->id;
59 | // $val['key'] = $this->key;
60 | // $val['name'] = $this->name;
61 | // $val['Contact'] = $this->contact;
62 | // $val['item_list'] = $this->item_list;
63 |
64 | $this->rdb->set($this->key, $val, [ 'ex' => 86400 ]);
65 |
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/lib/Controller/API/B2C.php:
--------------------------------------------------------------------------------
1 | auth_parse();
16 |
17 | $source_data = $this->parseJSON();
18 | if (empty($source_data['license']['id'])) {
19 | return $this->failure($RES, 'Invalid License ID [A7B-016]', 400);
20 | }
21 |
22 | $dbc = _dbc($this->Company['dsn']);
23 |
24 | $b2c = new \OpenTHC\POS\B2C\Sale($dbc);
25 | $b2c['license_id'] = $source_data['license']['id'];
26 | $b2c->save();
27 |
28 | // Format Output
29 | $ret = $b2c->toArray();
30 | $ret['license'] = [
31 | 'id' => $ret['license_id']
32 | ];
33 | unset($ret['license_id']);
34 |
35 | __exit_json([
36 | 'data' => $ret
37 | , 'meta' => []
38 | ]);
39 |
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/lib/Controller/API/B2C/Item.php:
--------------------------------------------------------------------------------
1 | auth_parse();
15 |
16 | if ( ! preg_match('/^01\w{24}$/', $ARG['id'])) {
17 | return $this->failure($RES, 'Invalid Request [ABI-015]', 400);
18 | }
19 |
20 | $source_data = $this->parseJSON();
21 |
22 | if (empty($source_data['license']['id'])) {
23 | return $this->failure($RES, 'Invalid License ID [ABI-021]', 400);
24 | }
25 | if (empty($source_data['inventory']['id'])) {
26 | return $this->failure($RES, 'Invalid Inventory [ABI-024]', 400);
27 | }
28 | if (empty($source_data['unit_count'])) {
29 | return $this->failure($RES, 'Invalid Request [ABI-027]', 400);
30 | }
31 |
32 |
33 | $dbc = _dbc($this->Company['dsn']);
34 |
35 | // Load Sale
36 | $b2c = new \OpenTHC\POS\B2C\Sale($dbc, $ARG['id']);
37 | if (empty($b2c['id'])) {
38 | return $this->failure($RES, 'Invalid Sale ID [ABI-032]', 400);
39 | }
40 |
41 | // Check Inventory
42 | $inv = new \OpenTHC\POS\Inventory($dbc, $source_data['inventory']['id']);
43 | if (empty($inv['id'])) {
44 | return $this->failure($RES, 'Invalid Inventory [ABI-038]', 400);
45 | }
46 |
47 | if (empty($source_data['unit_price'])) {
48 | $source_data['unit_price'] = $inv['sell'];
49 | }
50 |
51 | if ($inv['qty'] < $source_data['unit_count']) {
52 | return $this->failure($RES, 'Invalid Quantity [ABI-046]', 400);
53 | }
54 |
55 | // Add Item
56 | $b2c_item = new \OpenTHC\POS\B2C\Sale\Item($dbc);
57 | $b2c_item['b2c_sale_id'] = $b2c['id'];
58 | $b2c_item['inventory_id'] = $source_data['inventory']['id'];
59 | $b2c_item['unit_count'] = floatval($source_data['unit_count']);
60 | $b2c_item['unit_price'] = floatval($source_data['unit_price']);
61 | // $b2c_item['full_price'] = $b2c_item['unit_count'] * $b2c_item['unit_price'];
62 | $b2c_item->save();
63 |
64 | __exit_json([
65 | 'data' => $b2c_item->toArray()
66 | , 'meta' => []
67 | ]);
68 |
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/lib/Controller/API/B2C/Payment.php:
--------------------------------------------------------------------------------
1 | auth_parse();
16 |
17 | if ( ! preg_match('/^01\w{24}$/', $ARG['id'])) {
18 | return $this->failure($RES, 'Invalid Request [ABP-015]', 400);
19 | }
20 |
21 | $source_data = $this->parseJSON();
22 | if (empty($source_data['license']['id'])) {
23 | return $this->failure($RES, 'Invalid License ID [ABP-020]', 400);
24 | }
25 |
26 | $dbc = _dbc($this->Company['dsn']);
27 |
28 | $b2c = new \OpenTHC\POS\B2C\Sale($dbc);
29 | if (empty($b2c['id'])) {
30 | return $this->failure($RES, 'Not Found [ABP-027]', 404);
31 | }
32 |
33 | // $b2c_ledger = new
34 | $b2c_ledger['']
35 |
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/Controller/API/B2C/Receipt.php:
--------------------------------------------------------------------------------
1 | getItems();
24 | foreach ($b2c_item_list as $i => $b2ci) {
25 | $b2c_item_list[$i]['Inventory'] = new \OpenTHC\POS\Inventory($dbc, $b2ci['inventory_id']);
26 | }
27 |
28 | }
29 |
30 | /**
31 | * Generate a Preview Document
32 | */
33 | function preview($REQ, $RES, $ARG)
34 | {
35 |
36 | $this->auth_parse();
37 |
38 | $b2c = [
39 | 'id' => 'PREVIEW',
40 | 'created_at' => '1969-04-20T16:20:00 America/Los_Angeles',
41 | 'base_price' => 0,
42 | 'full_price' => 0,
43 | 'item_count' => 0,
44 | 'meta' => [
45 | 'cash_incoming' => 0,
46 | 'cash_outgoing' => 0,
47 | ]
48 | ];
49 |
50 | $max = rand(1, 10);
51 | $b2c_item_list = [];
52 | for ($idx=0; $idx<$max; $idx++) {
53 |
54 | $c = rand(1, 10);
55 | $p = rand(200, 10000) / 100;
56 |
57 | $b2c['base_price'] += ($c * $p);
58 |
59 | $b2c_item_list[] = [
60 | 'Inventory' => [
61 | 'guid' => ULID::create()
62 | ],
63 | 'Inventory' => [],
64 | 'Product' => [
65 | 'name' => 'Text/Product'
66 | ],
67 | 'Variety' => [
68 | 'name' => 'Text/Variety',
69 | ],
70 | 'unit_count' => $c,
71 | 'unit_price' => $p,
72 | 'base_price' => ($c * $p),
73 | 'full_price' => ($c * $p),
74 | ];
75 | }
76 |
77 | $dbc = _dbc($this->Company['dsn']);
78 |
79 | $pdf = new \OpenTHC\POS\PDF\Receipt();
80 | $pdf->setCompany( new \OpenTHC\Company($dbc, $this->Company ));
81 | $pdf->setLicense( new \OpenTHC\Company($dbc, $this->License ));
82 | // Options
83 | $pdf->head_text = json_decode($dbc->fetchOne('SELECT val FROM base_option WHERE key = :k', [ ':k' => sprintf('/%s/receipt/head', $this->License['id']) ]));
84 | $pdf->foot_text = json_decode($dbc->fetchOne('SELECT val FROM base_option WHERE key = :k', [ ':k' => sprintf('/%s/receipt/foot', $this->License['id']) ]));
85 | $pdf->foot_link = json_decode($dbc->fetchOne('SELECT val FROM base_option WHERE key = :k', [ ':k' => sprintf('/%s/receipt/link', $this->License['id']) ]));
86 | $pdf->tail_text = json_decode($dbc->fetchOne('SELECT val FROM base_option WHERE key = :k', [ ':k' => sprintf('/%s/receipt/tail', $this->License['id']) ]));
87 |
88 | $pdf->setSale($b2c);
89 | $pdf->setItems($b2c_item_list);
90 | $pdf->render();
91 | $name = sprintf('Receipt_%s.pdf', $b2c['id']);
92 | $pdf->Output($name, 'I');
93 |
94 | exit(0);
95 |
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/lib/Controller/API/B2C/Single.php:
--------------------------------------------------------------------------------
1 | auth_parse();
18 |
19 | if ( ! preg_match('/^01\w{24}$/', $ARG['id'])) {
20 | __exit_json([
21 | 'data' => null
22 | , 'meta' => [ 'detail' => 'Invalid Request [ABS-020]' ]
23 | ], 400);
24 | }
25 |
26 | $dbc = _dbc($this->Company['dsn']);
27 |
28 | // Fetch the Specified B2C Obbject
29 | $b2c = new \OpenTHC\POS\B2C\Sale($dbc, $ARG['id']);
30 | if (empty($b2c['id'])) {
31 | __exit_json([
32 | 'data' => null
33 | , 'meta' => [ 'detail' => 'Not Found [ABS-029]' ]
34 | ], 404);
35 | }
36 |
37 | $b2c_item_list = $b2c->getItems();
38 |
39 | // Format Output
40 | $ret = $b2c->toArray();
41 | $ret['license'] = [ 'id' => $ret['license_id'] ];
42 | unset($ret['license_id']);
43 | $ret['item_list'] = $b2c_item_list;
44 |
45 | __exit_text([
46 | 'data' => $ret
47 | , 'meta' => []
48 | ]);
49 | }
50 |
51 | /**
52 | *
53 | */
54 | function commit($REQ, $RES, $ARG)
55 | {
56 | $this->auth_parse();
57 |
58 | // Check ID
59 | if (!preg_match('/^01\w{24}$/', $ARG['id'])) {
60 | return $this->failure($RES, 'Invalid Request [ABS-055]', 400);
61 | }
62 |
63 | $source_data = $this->parseJSON();
64 | if (empty($source_data['license']['id'])) {
65 | return $this->failure($RES, 'Invalid Request [ABS-060]', 400);
66 | }
67 |
68 | $dbc = _dbc($this->Company['dsn']);
69 | $dbc->query('BEGIN');
70 |
71 | // Fetch the Specified B2C Obbject
72 | $b2c = new \OpenTHC\POS\B2C\Sale($dbc, $ARG['id']);
73 | if (empty($b2c['id'])) {
74 | return $this->failure($RES, 'Not Found [ABS-063]', 404);
75 | }
76 | if (100 != $b2c['stat']) {
77 | return $this->failure($RES, 'Invalid B2C Sale State [ABS-072]', 404);
78 | }
79 |
80 | // Get Items and Decrement
81 | $sum_item_price = 0;
82 | $b2c_item_list = $b2c->getItems();
83 | foreach ($b2c_item_list as $b2c_item) {
84 | $Inv = new \OpenTHC\POS\Inventory($dbc, $b2c_item['inventory_id']);
85 | try {
86 | $Inv->decrement($b2c_item['unit_count']);
87 | } catch (\Exception $e) {
88 | // Ignore
89 | }
90 | $sum_item_price += ($b2c_item['unit_count'] * $b2c_item['unit_price']);
91 |
92 | // Load Taxes And Add b2c_item_adjust
93 |
94 | }
95 | $b2c['base_price'] = $sum_item_price;
96 | $b2c['stat'] = 200;
97 | $b2c->save('B2C/Sale/Commit via API');
98 |
99 | $dbc->query('COMMIT');
100 |
101 | // @todo send to the CRE, or just add to the pump?
102 |
103 | return $RES->withJSON([
104 | 'data' => $b2c->toArray()
105 | , 'meta' => []
106 | ]);
107 |
108 | }
109 |
110 | /**
111 | * Verify the Transaction
112 | */
113 | function verify($REQ, $RES, $ARG)
114 | {
115 | $this->auth_parse();
116 |
117 | // Check ID
118 | if (!preg_match('/^01\w{24}$/', $ARG['id'])) {
119 | return $this->failure($RES, 'Invalid Request [ABS-073]', 400);
120 | }
121 |
122 | $dbc = _dbc($this->Company['dsn']);
123 |
124 | // Fetch the Specified B2C Obbject
125 | $b2c = new \OpenTHC\POS\B2C\Sale($dbc, $ARG['id']);
126 | if (empty($b2c['id'])) {
127 | return $this->failure($RES, 'Not Found [ABS-081]', 400);
128 | }
129 |
130 | // $b2c_item_list = $b2c->getItems();
131 |
132 | // Fetch Product Type Limits
133 | // $res_product_limit = $dbc->fetchAll('SELECT * FROM b2c_sale_product_limit WHERE ')
134 |
135 |
136 | // __exit_json([
137 | // 'data' => null
138 | // , 'meta' => [ 'detail' => 'Not Implemented [ABS-063]' ]
139 | // ], 501);
140 |
141 | return $RES->withJSON([
142 | 'data' => null
143 | , 'meta' => [ 'detail' => 'No Limits Apply' ]
144 | ]);
145 | }
146 |
147 | }
148 |
--------------------------------------------------------------------------------
/lib/Controller/API/Base.php:
--------------------------------------------------------------------------------
1 | $ERR,
25 | 'meta' => [ 'note' => 'Fatal Error [ERR-001]' ]
26 | ], 500);
27 | };
28 | };
29 |
30 | $c['errorHandler'] = function($c) {
31 | return function($REQ, $RES, $ERR) {
32 |
33 | $ret_code = 500;
34 | $err_code = $ERR->getCode();
35 | if (($err_code >= 200) && ($err_code <= 599)) {
36 | $ret_code = $err_code;
37 | }
38 |
39 | __exit_json([
40 | 'data' => $ERR,
41 | 'meta' => [
42 | 'note' => 'Fatal Error [ERR-002]',
43 | 'error' => $ERR->getMessage(),
44 | ]
45 | ], $ret_code);
46 | };
47 | };
48 |
49 | // Over-ride the Response object
50 | // FAIL: Pimple\Exception\FrozenServiceException: Cannot override frozen service "response".
51 | // $c['response'] = function($c) {
52 | // $ret = new class extends \Slim\Http\Response {
53 | // function withJSON($json, $code=200, $flag=0)
54 | // {
55 | // $flag = intval($flag);
56 | // return parent::withJSON($json, $code, ($flag | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
57 | // }
58 | // };
59 | // return $ret;
60 | // };
61 |
62 | parent::__construct($c);
63 |
64 | }
65 |
66 | /**
67 | * This maybe should be in Middleware
68 | */
69 | function auth_parse()
70 | {
71 | $auth = $_SERVER['HTTP_AUTHORIZATION'];
72 | if ( ! preg_match('/^Bearer v2024\/([\w\-]{43})\/([\w\-]+)$/', $auth, $m)) {
73 | __exit_json(array(
74 | 'data' => null,
75 | 'meta' => [ 'note' => 'Invalid Authorization [CAB-068]' ],
76 | ), 403);
77 | }
78 |
79 | $act = $this->open_auth_box($m[1], $m[2]);
80 |
81 | $dbc_auth = _dbc('auth');
82 |
83 | // Find Service Lookup CPK and See if we Trust Them
84 | $Service = $this->findService($dbc_auth, $act->pk);
85 | $Contact = $this->findContact($dbc_auth, $act->contact);
86 | $Company = $this->findCompany($dbc_auth, $act->company);
87 |
88 | $dbc_user = _dbc($Company['dsn']);
89 | $License = $this->findLicense($dbc_user, $act->license);
90 |
91 | $this->Service = $Service;
92 | $this->Contact = $Contact;
93 | $this->Company = $Company;
94 | $this->License = $License;
95 |
96 | }
97 |
98 | /**
99 | *
100 | */
101 | function failure($RES, $text, $code=500)
102 | {
103 | return $RES->withJSON([
104 | 'data' => null
105 | , 'meta' => [ 'note' => $text ]
106 | ], $code, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
107 | }
108 |
109 | /**
110 | *
111 | */
112 | function __invoke($REQ, $RES, $ARG)
113 | {
114 | $this->auth_parse();
115 |
116 | __exit_json([
117 | 'data' => $_SERVER,
118 | 'meta' => [ 'note' => 'Not Implemented [CAB-098]' ],
119 | ], 501);
120 |
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/lib/Controller/API/Main.php:
--------------------------------------------------------------------------------
1 | fetchRow('SELECT * FROM auth_company WHERE id = :c0', [ ':c0' => $_SESSION['Company']['id'] ]);
27 | if (empty($Company0['dsn'])) {
28 | throw new \Exception('Fatal Database Error [CAC-043]', 500);
29 | }
30 |
31 | if (empty($Company0['cre'])) {
32 | throw new \Exception('Company Configuration requires CRE [CAC-030]', 500);
33 | }
34 |
35 | $dbc_user = _dbc($Company0['dsn']);
36 | $Company1 = $dbc_user->fetchRow('SELECT * FROM auth_company WHERE id = :c0', [ ':c0' => $_SESSION['Company']['id'] ]);
37 | $_SESSION['dsn'] = $Company0['dsn'];
38 | unset($Company0['dsn']);
39 |
40 | $_SESSION['Company'] = array_merge($Company0, $Company1);
41 | $_SESSION['Company']['cre_meta'] = json_decode($_SESSION['Company']['cre_meta'], true);
42 |
43 | // Load License
44 | // Maybe offer a License Picker?
45 | // return $RES->withRedirect('/auth/license/select');
46 | if ( ! empty($_SESSION['License']['id'])) {
47 | // Reload License
48 | $_SESSION['License'] = $this->findLicense($dbc_user, $_SESSION['License']['id']);
49 | } else {
50 | // Find Default
51 | $sql = <<fetchRow($sql, [
59 | ':f1' => 0x01000000
60 | ]);
61 | $_SESSION['License'] = $License;
62 | }
63 |
64 | // Save the CRE Stuff?
65 | $_SESSION['cre'] = \OpenTHC\CRE::getEngine($_SESSION['Company']['cre']);
66 |
67 | // Cleanup some legacy CRE data
68 | if (empty($_SESSION['cre']['license']) && !empty($_SESSION['cre']['auth']['license'])) {
69 | $_SESSION['cre']['license'] = $_SESSION['cre']['auth']['license'];
70 | unset($_SESSION['cre']['auth']['license']);
71 | }
72 |
73 | unset($_SESSION['cre']['auth']);
74 |
75 | return $RES->withRedirect($ret);
76 |
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/lib/Controller/Auth/Shut.php:
--------------------------------------------------------------------------------
1 | getStatusCode()) {
16 | return $res0;
17 | }
18 |
19 | $data = [];
20 | $data['Page'] = [ 'title' => 'Session Closed' ];
21 | $data['body'] = 'Your session has been closed
';
22 | $data['foot'] = 'Sign In Again';
23 |
24 | return $RES->write( $this->render('done.php', $data) );
25 |
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/lib/Controller/Auth/oAuth2/Back.php:
--------------------------------------------------------------------------------
1 | getProvider();
18 |
19 | if (empty($_GET['code'])) {
20 | __exit_text('Invalid Link [AOB-018]', 400);
21 | }
22 |
23 | // Check State
24 | $this->checkState();
25 |
26 | $tok = null;
27 |
28 | // Try to get an access token using the authorization code grant.
29 | try {
30 | $tok = $p->getAccessToken('authorization_code', [
31 | 'code' => $_GET['code']
32 | ]);
33 | } catch (\Exception $e) {
34 | __exit_text('Invalid Access Token [AOB-027]', 400);
35 | }
36 |
37 | if (empty($tok)) {
38 | __exit_text('Invalid Access Token [AOB-031]', 400);
39 | }
40 |
41 | $chk = json_decode(json_encode($tok), true);
42 | if (empty($chk['access_token'])) {
43 | __exit_text('Invalid Access Token [AOB-036]', 400);
44 | }
45 | if (empty($chk['scope'])) {
46 | __exit_text('Invalid Access Token [AOB-039]', 400);
47 | }
48 | if (empty($chk['token_type'])) {
49 | __exit_text('Invalid Access Token [AOB-042]', 400);
50 | }
51 | if ('bearer' != $chk['token_type']) {
52 | __exit_text('Invalid Access Token [AOB-045]', 400);
53 | }
54 |
55 | // Using the access token, we may look up details about the
56 | // resource owner.
57 | try {
58 |
59 | $x = $p->getResourceOwner($tok)->toArray();
60 |
61 | $_SESSION['Company'] = $x['Company'];
62 | $_SESSION['Contact'] = $x['Contact'];
63 | $_SESSION['License'] = $x['License'];
64 | $_SESSION['tz'] = $_SESSION['Company']['tz'];
65 |
66 | return $RES->withRedirect('/auth/init?' . http_build_query([
67 | 'r' => $_GET['r']
68 | ]));
69 |
70 | } catch (\Exception $e) {
71 | __exit_text($e->getMessage() . ' [AOB-071]', 500);
72 | }
73 |
74 | __exit_text('Invalid Request [AOB-072]', 400);
75 | }
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/lib/Controller/Auth/oAuth2/Open.php:
--------------------------------------------------------------------------------
1 | _get_return_path();
29 |
30 | if ( ! empty($_GET['box'])) {
31 |
32 | $box = $_GET['box'];
33 |
34 | if (preg_match('/^v2024\/([\w\-]{43})\/([\w\-]+)$/', $box, $m)) {
35 |
36 | $this->dbc = _dbc('auth');
37 | $act = $this->open_auth_box($m[1], $m[2]);
38 | $Service = $this->findService($this->dbc, $act->pk);
39 | $Contact = $this->findContact($this->dbc, $act->contact);
40 | $Company = $this->findCompany($this->dbc, $act->company);
41 |
42 | $_SESSION['Contact'] = $Contact;
43 | $_SESSION['Company'] = $Company;
44 | $_SESSION['License'] = [
45 | 'id' => $act->license
46 | ];
47 |
48 | return $RES->withRedirect('/auth/init');
49 |
50 | }
51 | }
52 |
53 | // @deprecated
54 | if ( ! empty($_GET['jwt'])) {
55 |
56 | // $p = $this->getProvider();
57 | $sso = new \OpenTHC\Service('sso');
58 | $res = $sso->post('/api/jwt/verify', [ 'form_params' => [ 'token' => $_GET['jwt'] ] ]);
59 | // switch ($res['code']) {
60 | // case 200:
61 | // // OK
62 | // default:
63 | // return $RES->withJSON(['meta' => [ 'note' => 'Invalid Token [AOO-033]' ]], 400);
64 | // }
65 |
66 | $dbc = _dbc('auth');
67 |
68 | try {
69 |
70 | $chk = JWT::decode_only($_GET['jwt']);
71 | $key = $dbc->fetchOne('SELECT hash FROM auth_service WHERE id = :s0', [ ':s0' => $chk->body->iss ]);
72 | $jwt = JWT::verify($_GET['jwt'], $key);
73 |
74 | $_SESSION['Contact'] = [
75 | 'id' => $jwt->sub,
76 | ];
77 | if (empty($_SESSION['Contact']['id'])) {
78 | return $RES->withJSON(['meta' => [ 'note' => 'Invalid Contact [AOO-035]' ]], 400);
79 | }
80 |
81 | $_SESSION['Company'] = [
82 | 'id' => $jwt->company,
83 | ];
84 | if (empty($_SESSION['Company']['id'])) {
85 | return $RES->withJSON(['meta' => [ 'note' => 'Invalid Company [AOO-042]' ]], 400);
86 | }
87 |
88 | $_SESSION['License'] = [
89 | 'id' => $jwt->license,
90 | ];
91 | if (empty($_SESSION['License']['id'])) {
92 | return $RES->withJSON(['meta' => [ 'note' => 'Invalid License [AOO-049]' ]], 400);
93 | }
94 |
95 | return $RES->withRedirect('/auth/init');
96 |
97 | } catch (\Exception $e) {
98 | // What?
99 | }
100 |
101 | return $RES->withRedirect($ret_path);
102 |
103 | }
104 |
105 | $p = $this->getProvider($ret_path);
106 | $url = $p->getAuthorizationUrl([
107 | 'scope' => 'contact company license pos',
108 | ]);
109 |
110 | $_SESSION['oauth2-state'] = $p->getState();
111 |
112 | return $RES->withRedirect($url);
113 |
114 | }
115 |
116 | /**
117 | *
118 | */
119 | function _get_return_path()
120 | {
121 | $ret = '/dashboard';
122 | if (!empty($_GET['r'])) {
123 | switch ($_GET['r']) {
124 | case '1':
125 | case 'r':
126 | // @todo should validate the referrer
127 | $ret = $_SERVER['HTTP_REFERER'];
128 | break;
129 | default:
130 | $ret = $_GET['r'];
131 | break;
132 | }
133 | }
134 |
135 | return $ret;
136 |
137 | }
138 |
139 | }
140 |
--------------------------------------------------------------------------------
/lib/Controller/CRM/Contact.php:
--------------------------------------------------------------------------------
1 | array('title' => 'CRM :: Contact List'),
18 | );
19 |
20 | $dbc = $this->_container->DB;
21 | $sql = 'SELECT * FROM contact WHERE email IS NOT NULL OR phone IS NOT NULL ORDER BY id LIMIT 50';
22 | $data['contact_list'] = $dbc->fetchAll($sql);
23 |
24 | return $RES->write( $this->render('crm/contact.php', $data) );
25 |
26 | }
27 |
28 | function save($REQ, $RES, $ARG)
29 | {
30 |
31 | $dbc = $this->_container->DB;
32 |
33 | $rec = [
34 | 'id' => ULID::create(),
35 | 'fullname' => trim($_POST['contact-name']),
36 | 'email' => trim(strtolower($_POST['contact-email'])),
37 | 'phone' => trim($_POST['contact-phone']),
38 | 'hash' => '-',
39 | // 'tags' =>
40 | ];
41 | $rec['guid'] = $rec['id'];
42 |
43 | $dbc->insert('contact', $rec);
44 |
45 | return $RES->withRedirect('/crm/contact');
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/lib/Controller/CRM/Main.php:
--------------------------------------------------------------------------------
1 | array('title' => 'CRM'),
16 | );
17 |
18 | return $RES->write( $this->render('crm/main.php', $data) );
19 |
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/lib/Controller/CRM/Message.php:
--------------------------------------------------------------------------------
1 | array('title' => 'CRM :: Messaging'),
14 | );
15 |
16 | return $RES->write( $this->render('crm/message.php', $data) );
17 |
18 | }
19 |
20 | function email($REQ, $RES, $ARG)
21 | {
22 | $data = array(
23 | 'Page' => array('title' => 'CRM :: Messaging :: Compose Email'),
24 | );
25 |
26 | return $RES->write( $this->render('crm/message-compose-email.php', $data) );
27 |
28 | }
29 |
30 | function sms($REQ, $RES, $ARG)
31 | {
32 | $data = array(
33 | 'Page' => array('title' => 'CRM :: Messaging :: Compose SMS'),
34 | );
35 |
36 | return $RES->write( $this->render('crm/message-compose-sms.php', $data) );
37 |
38 | }
39 |
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/lib/Controller/Contact.php:
--------------------------------------------------------------------------------
1 | _container->DB;
18 |
19 | $res_contact = $dbc->fetchAll('SELECT id, fullname FROM contact WHERE guid LIKE :g0 OR code LIKE :c0', [
20 | ':g0' => sprintf('%%%s%%', $_GET['term']),
21 | ':c0' => sprintf('%%%s%%', $_GET['term']),
22 | ]);
23 |
24 | $ret = [];
25 | foreach ($res_contact as $c) {
26 | $ret[] = [
27 | 'id' => $c['id'],
28 | 'label' => $c['fullname'],
29 | 'value' => $c['fullname'],
30 | ];
31 | }
32 |
33 | return $RES->withJSON($ret);
34 |
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/Controller/Dashboard/Main.php:
--------------------------------------------------------------------------------
1 | array('title' => 'Dashboard'),
18 | );
19 |
20 | $dbc_user = $this->_container->DB;
21 | $license_list = $dbc_user->fetchAll('SELECT * FROM license WHERE flag & :f1 = :f1', [
22 | ':f1' => \OpenTHC\License::FLAG_MINE,
23 | ]);
24 |
25 | foreach ($license_list as $l) {
26 | if ('retail' == $l['type']) {
27 | $_SESSION['License'] = $l;
28 | break;
29 | }
30 | }
31 |
32 | // if (empty($_SESSION['License'])) {
33 | // return $RES->write( $this->render('pick-license.php', $data) );
34 | // }
35 |
36 | return $RES->write( $this->render('dashboard.php', $data) );
37 |
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/lib/Controller/Intent.php:
--------------------------------------------------------------------------------
1 | [ 'title' => 'Intent' ],
19 | ];
20 |
21 | switch ($_GET['a']) {
22 | case 'delivery-auth':
23 | return $this->delivery_auth($RES);
24 | break;
25 | case 'vendor-view':
26 | return $RES->write( $this->render('intent/vendor-view.php', $data) );
27 | break;
28 | }
29 |
30 | return $RES->write( $this->render('intent/main.php', $data) );
31 |
32 | }
33 |
34 | /**
35 | *
36 | */
37 | function delivery_auth($RES)
38 | {
39 | $data = $this->data;
40 |
41 | // $sso_link = sprintf('/auth/open?r=/')
42 |
43 | // if (empty($_POST['contact-email'])) {
44 | // return $RES->write( $this->render('intent/delivery-auth-contact.php', $data) );
45 | // }
46 |
47 | if ('auth-code' == $_POST['a']) {
48 |
49 | // \CSRF::verify($_POST['CSRF']);
50 |
51 | // Company Database
52 | $dbc_auth = _dbc('auth');
53 | $dsn = $dbc_auth->fetchOne('SELECT dsn FROM auth_company WHERE id = :c0', [ ':c0' => $_GET['c'] ]);
54 | if (empty($dsn)) {
55 | _exit_html_warn('
Invalid Request [LCI-025]
', 400);
56 | }
57 |
58 | // Contact Search by PIN
59 | $dbc = _dbc($dsn);
60 | $hash = md5(preg_replace('/[^\d]+/', '', $_POST['code']));
61 | $chk = $dbc->fetchRow('SELECT id, username FROM auth_contact WHERE username = :c1 AND passcode = :p1', [
62 | ':c1' => $_SESSION['intent-delivery-username'],
63 | ':p1' => $hash,
64 | ]);
65 | if (empty($chk['id'])) {
66 | _exit_html_warn('Invalid Request [LCI-035]
', 400);
67 | }
68 | // We should have a cookie, with encrypted stuff?
69 | // Some kind of ID because we need to have unique passcode Too
70 |
71 | // Long-Term Marking as Delivery Auth OK?
72 | if ($_COOKIE['pos-contact']) {
73 | // Do Something Smart?
74 | }
75 |
76 | $_SESSION['Company'] = [];
77 | $_SESSION['Contact'] = [];
78 | $_SESSION['License'] = [];
79 |
80 | $_SESSION['Company']['id'] = $_GET['c'];
81 | $_SESSION['License']['id'] = $_GET['l'];
82 | $_SESSION['Contact']['id'] = $chk['id'];
83 |
84 | // $tok = $dbc_auth->insert('auth_context_ticket', [
85 | // 'id' => _random_hash(),
86 | // 'meta' => json_encode([
87 | // 'intent' => 'auth-open-delivery',
88 | // 'company' => $_GET['c'],
89 | // 'contact' => $chk['id'],
90 | // ])
91 | // ]);
92 |
93 | return $RES->withRedirect(sprintf('/auth/init?_=%s&r=/delivery/live', $tok));
94 |
95 | }
96 |
97 | return $RES->write( $this->render('intent/delivery-auth.php', $data) );
98 |
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/lib/Controller/Menu/Main.php:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openthc/pos/d54b19901e8d4e075a7a8802eaca36e1bf9ef7f6/lib/Controller/Menu/Main.php
--------------------------------------------------------------------------------
/lib/Controller/POS/Cart/Drop.php:
--------------------------------------------------------------------------------
1 | _container->DB;
18 |
19 | $sql = 'DELETE FROM b2c_sale_hold WHERE id = ?';
20 | $arg = [ $_POST['cart'] ];
21 | $dbc->query($sql, $arg);
22 |
23 | return $RES->withJSON(null, 204);
24 |
25 | }
26 | _exit_json([
27 | 'meta' => [ 'detail' => 'Invalid Input [PCD#023]' ],
28 | ], 400);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/Controller/POS/Cart/Open.php:
--------------------------------------------------------------------------------
1 | fetchRow('SELECT * FROM b2c_sale_hold WHERE id = ?', $arg);
21 | $SH['json'] = json_decode($SH['json'], true);
22 |
23 |
24 | echo '';
42 |
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/lib/Controller/POS/Cart/Save.php:
--------------------------------------------------------------------------------
1 | _container->DB;
24 |
25 | $idx_item = 0;
26 | foreach ($_POST as $k => $v) {
27 | if (preg_match('/^qty\-(\w{26})$/', $k, $m)) {
28 | $idx_item++;
29 | }
30 | }
31 | if ($idx_item == 0) {
32 | return $RES->withRedirect('/pos');
33 | }
34 |
35 | $sql = 'INSERT INTO b2c_sale_hold (contact_id, meta) VALUES (:c0, :m1) RETURNING id';
36 | $arg = [
37 | ':c0' => $_SESSION['Contact']['id'],
38 | // ':t1' => 'general',
39 | ':m1' => json_encode($_POST),
40 | ];
41 | $hid = $dbc->fetchOne($sql, $arg);
42 | if (empty($hid)) {
43 | Session::flash('fail', 'Failed to place hold');
44 | return $RES->withRedirect('/pos');
45 | }
46 | Session::flash('info', sprintf('Hold #%d Confirmed', $hid));
47 |
48 | if ($auto_print_ticket) {
49 | // @todo Fire & Forget and HTTP Print Request to ... ?
50 | // HTTP::post('/api/print', array('object' => 'hold', 'object-id' => $hid));
51 | }
52 |
53 | return $RES->withRedirect('/pos');
54 |
55 | }
56 |
57 | Session::flash('fail', 'Invalid Input [PCS-055]');
58 | return $RES->withRedirect('/pos');
59 |
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/lib/Controller/POS/Checkout/Done.php:
--------------------------------------------------------------------------------
1 | array('title' => 'POS :: Checkout :: Done')
16 | );
17 |
18 | return $RES->write( $this->render('pos/checkout/done.php', $data) );
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/lib/Controller/POS/Delivery.php:
--------------------------------------------------------------------------------
1 | ['title' => 'POS :: Delivery' ],
16 | 'b2c_sale_hold' => [],
17 | 'contact_list' => [], // Employee Contacts
18 | 'courier_list' => [], // Active Courier Contacts
19 | ];
20 |
21 | $dbc = $this->_container->DB;
22 |
23 | // Select Couriers
24 | $sql = "SELECT id, fullname AS name FROM contact";
25 | $data['contact_list'] = $dbc->fetchAll($sql);
26 |
27 | // Contacts are insert/delete from this table when active-on-shift
28 | $sql = "SELECT * FROM b2c_courier";
29 | // $res = $dbc->fetchAll($sql);
30 | $data['courier_list'] = [
31 | [
32 | 'id' => 'ABC',
33 | 'name' => 'David Busby'
34 | , 'stat' => 200
35 | , 'ping' => 240
36 | , 'location' => '53rd & 3rd'
37 | ],
38 | [
39 | 'id' => 'DEF',
40 | 'name' => 'Bavid Dusby'
41 | , 'stat' => 200
42 | , 'ping' => 420
43 | , 'location' => '22 Acacia Avenue'
44 | ]
45 | ];
46 |
47 |
48 | // Select Orders
49 | $sql = <<fetchAll($sql);
57 |
58 | $data['map_api_key_js'] = \OpenTHC\Config::get('google/map_api_key_js');
59 |
60 | return $RES->write( $this->render('pos/delivery.php', $data) );
61 | }
62 |
63 | /**
64 | *
65 | */
66 | function ajax($REQ, $RES, $ARG)
67 | {
68 | switch ($_GET['a']) {
69 | case 'delivery-auth':
70 | $link = sprintf('https://%s/intent?%s'
71 | , $_SERVER['SERVER_NAME']
72 | , http_build_query([
73 | 'a' => 'delivery-auth',
74 | 'c' => $_SESSION['Company']['id'],
75 | 'l' => $_SESSION['License']['id']
76 | ])
77 | );
78 | __exit_json([
79 | 'data' => $link
80 | ]);
81 |
82 | }
83 |
84 | __exit_json([
85 | 'data' => null,
86 | 'meta' => [ 'detail' => 'Error' ]
87 | ], 400);
88 |
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/lib/Controller/POS/Main.php:
--------------------------------------------------------------------------------
1 | _container->DB;
23 |
24 | $sql = << 0
30 | AND sell IS NOT NULL
31 | AND sell > 0
32 | SQL;
33 | $arg = [
34 | ':l0' => $_SESSION['License']['id']
35 | ];
36 | $chk = $dbc->fetchOne($sql, $arg);
37 | if (empty($chk)) {
38 | _exit_html_fail('Inventory Lots need to be present and priced for the POS to operate [CPH-020]
', 501);
39 | }
40 |
41 | if (empty($_SESSION['pos-terminal-id'])) {
42 | $_SESSION['pos-terminal-id'] = ULID::create();
43 | }
44 |
45 | if (empty($_SESSION['pos-terminal-contact'])) {
46 | $data = [];
47 | $data['Page'] = [ 'title' => 'Terminal Authentication'];
48 | return $RES->write( $this->render('pos/open.php', $data) );
49 | }
50 |
51 | // Page Data
52 | $data = array(
53 | 'Page' => array('title' => sprintf('POS :: %s %s
', $_SESSION['License']['name'], $_SESSION['License']['code']))
54 | );
55 |
56 | $Cart = new \OpenTHC\POS\Cart($this->_container->Redis, $_GET['cart']);
57 |
58 | if (empty($Cart->Contact)) {
59 | return $RES->write( $this->render('pos/contact-select.php', $data) );
60 | }
61 |
62 | if ($Cart->Contact->stat != Contact::STAT_LIVE) {
63 | return $RES->write( $this->render('pos/contact-verify.php', $data) );
64 | }
65 |
66 | $data['cart'] = $Cart;
67 |
68 | return $RES->write( $this->render('pos/terminal/main.php', $data) );
69 |
70 | }
71 |
72 | /**
73 | * POST Handler
74 | */
75 | function post($REQ, $RES, $ARG)
76 | {
77 | switch ($_POST['a']) {
78 | case 'auth-code':
79 |
80 | // Lookup Contact by this Auth Code
81 | $code = $_POST['code'];
82 |
83 | $dbc = $this->_container->DB;
84 |
85 | $Contact = $dbc->fetch_row('SELECT * FROM auth_contact WHERE auth_code = :a0', [
86 | ':a0' => $_POST['code'],
87 | ]);
88 | if ( ! empty($Contact['id'])) {
89 | __exit_text('Invalid Contact', 400);
90 | }
91 |
92 | // Assign to Register Session
93 | // Set Expiration in T minutes?
94 | $R = $this->_container->Redis;
95 | $k = sprintf('/%s/pos-terminal', $_SESSION['Contact']['id']);
96 | $v = $_SESSION['Contact']['id'];
97 | $R->set($k, $v, [ 'ex' => 600 ]);
98 |
99 | $_SESSION['pos-terminal-contact'] = $_SESSION['Contact']['id'];
100 |
101 | return $RES->withRedirect('/pos');
102 |
103 | break;
104 |
105 | }
106 |
107 | }
108 |
109 | private function openCart()
110 | {
111 | }
112 |
113 | }
114 |
--------------------------------------------------------------------------------
/lib/Controller/POS/Online.php:
--------------------------------------------------------------------------------
1 | ['title' => 'POS :: Online' ],
16 | ];
17 | $data['b2c_sale_hold'] = [];
18 |
19 | // Select
20 | $dbc = $this->_container->DB;
21 | $sql = <<fetchAll("SELECT * FROM b2c_sale_hold WHERE type = 'online'");
30 |
31 | return $RES->write( $this->render('pos/online.php', $data) );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/Controller/POS/Shut.php:
--------------------------------------------------------------------------------
1 | _container->Redis;
23 | // $R->hset(sprintf('pos-terminal-%s', $_SESSION['pos-terminal-id']), array(
24 | // 'ping' => $_SERVER['REQUEST_TIME'],
25 | // 'name' => $_SESSION['Company']['name'],
26 | // 'stat' => 'shut',
27 | // ));
28 |
29 | $data = [];
30 | $data['Page'] = [
31 | 'title' => 'POS :: Terminal :: Shut',
32 | ];
33 |
34 | return $RES->write( $this->render('pos/terminal/shut.php', $data) );
35 |
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/Controller/Report/Ajax.php:
--------------------------------------------------------------------------------
1 | _container->DB;
17 |
18 | switch ($_POST['a']) {
19 | case 'push-transaction':
20 | $B2C = $_POST['id'];
21 | $sql = 'SELECT * FROM b2c_sale WHERE id = :pk AND license_id = :l0';
22 | $B2C = $dbc->fetchRow($sql, [
23 | ':pk' => $B2C,
24 | ':l0' => $_SESSION['License']['id'],
25 | ]);
26 | $B2C = new \OpenTHC\POS\B2C\Sale($dbc, $B2C);
27 | if ($B2C['id']) {
28 | return $this->_push_transaction($RES, $B2C);
29 | }
30 |
31 | return $RES->withStatus(404);
32 | break;
33 | }
34 | }
35 |
36 | function _push_transaction($RES, $B2C)
37 | {
38 | $dbc = $this->_container->DB;
39 |
40 | $cfg = \OpenTHC\CRE::getEngine($_SESSION['cre']['id']);
41 | $cfg = array_merge($_SESSION['cre'], $cfg);
42 | $cre = \OpenTHC\CRE::factory($cfg);
43 | $cre->setLicense($_SESSION['License']);
44 |
45 | $meta = $B2C->getMeta();
46 | $item_list = $B2C->getItems();
47 | $b2c_sale_item_list = [];
48 | foreach ($item_list as $b2c_sale_item) {
49 | $I = new \OpenTHC\POS\Inventory($dbc, $b2c_sale_item['inventory_id']);
50 | $b2c_sale_item = new \OpenTHC\POS\B2C\Sale\Item($dbc, $b2c_sale_item);
51 | $b2c_sale_item_list[] = [
52 | 'inventory' => $I,
53 | 'sale_item' => $b2c_sale_item,
54 | ];
55 | }
56 |
57 | switch ($cre::ENGINE) {
58 | case 'metrc':
59 |
60 | $d = new \DateTime($B2C['created_at']);
61 | $obj = array(
62 | 'SalesDateTime' => $d->format(\DateTime::ISO8601),
63 |
64 | // @todo We need to have a UX for Patients and Caregivers with Identifiers
65 | // Requires: Correct Facility Type
66 | // 'SalesCustomerType' => 'Consumer', // @see GET /sales/v1/customertypes?
67 | // 'PatientLicenseNumber' => null,
68 | // 'CaregiverLicenseNumber' => null,
69 |
70 | // 'SalesCustomerType' => 'ExternalPatient', // @see GET /sales/v1/customertypes?
71 | // 'PatientLicenseNumber' => null,
72 | // 'CaregiverLicenseNumber' => null,
73 |
74 | // Requires: Patient Number
75 | // Requires: Caregiver Number
76 | // 'SalesCustomerType' => 'Caregiver', // @see GET /sales/v1/customertypes?
77 | // 'PatientLicenseNumber' => '000001',
78 | // 'CaregiverLicenseNumber' => '000002',
79 |
80 | // Requires: Patient Number
81 | 'SalesCustomerType' => 'Patient', // @see GET /sales/v1/customertypes?
82 | 'PatientLicenseNumber' => '000001',
83 | 'CaregiverLicenseNumber' => null,
84 |
85 | 'IdentificationMethod' => null,
86 | 'PatientRegistrationLocationId' => null,
87 | 'Transactions' => array(),
88 | );
89 | foreach ($b2c_sale_item_list as $b2c_sale_item) {
90 | $sale_item = $b2c_sale_item['sale_item'];
91 | $I = $b2c_sale_item['inventory'];
92 |
93 | $uom = new \OpenTHC\UOM($sale_item['uom']);
94 | $uom = $uom->getName();
95 |
96 | $transaction = array(
97 | 'PackageLabel' => $I['guid'],
98 | 'Quantity' => $sale_item['unit_count'],
99 | 'UnitOfMeasure' => $uom,
100 | 'TotalAmount' => ($sale_item['unit_price'] * $sale_item['unit_count']),
101 | 'UnitThcPercent' => null,
102 | 'UnitThcContent' => null,
103 | 'UnitThcContentUnitOfMeasure' => null,
104 | 'UnitWeight' => null,
105 | 'UnitWeightUnitOfMeasure' => null,
106 | );
107 | $obj['Transactions'][] = $transaction;
108 | }
109 |
110 | $res = $cre->b2c()->create($obj);
111 | switch ($res['code']) {
112 | case 200:
113 | // $chk = $cre->b2c()->sync(); // @todo
114 | // $chk = $cre->b2c()->transactions()->sync(); // @todo
115 | // $chk = $cre->inventory()->sync(); // @todo
116 | $RES = $RES->withStatus(201)
117 | ->withHeader('Content-type', 'application/json')
118 | ->withJson([
119 | 'data' => [
120 | 'b2c_list' => [$B2C->toArray()]
121 | ],
122 | 'meta' => [],
123 | ]);
124 | ;
125 | break;
126 |
127 | default:
128 | \Edoceo\Radix\Session::flash('fail', $cre->formatError());
129 | $RES = $RES->withStatus(502);
130 | }
131 |
132 | break;
133 |
134 | case 'openthc':
135 | default:
136 | }
137 |
138 | return $RES;
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/lib/Controller/Report/Main.php:
--------------------------------------------------------------------------------
1 | render('report/main.php', $data);
22 |
23 | return $RES->write($html);
24 |
25 | }
26 |
27 | /**
28 | *
29 | */
30 | function recent($REQ, $RES, $ARG)
31 | {
32 | $dbc = $this->_container->DB;
33 |
34 | $sql = 'SELECT * FROM b2c_sale WHERE license_id = :l0 AND stat = :s0';
35 | $b2c_transactions = $dbc->fetchAll($sql, [
36 | ':s0' => \OpenTHC\POS\B2C\Sale::STAT_OPEN,
37 | ':l0' => $_SESSION['License']['id'],
38 | ]);
39 | $data = [];
40 | $data['b2c_transactions'] = $b2c_transactions;
41 |
42 | $html = $this->render('report/recent.php', $data);
43 |
44 | return $RES->write($html);
45 |
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/lib/Controller/Shop/Base.php:
--------------------------------------------------------------------------------
1 | data;
21 | $data['Page']['title'] = 'Cart';
22 | $data['Company'] = [
23 | 'id' => $_GET['c'],
24 | ];
25 |
26 | $cart = sprintf('cart-%s', $data['Company']['id']);
27 |
28 | $product_list = $this->load_cart_product_list($data['Company']['id']);
29 |
30 | $data['product_list'] = [];
31 | foreach ($product_list as $p) {
32 | $data['product_list'][] = [
33 | 'inventory' => [
34 | 'id' => $p['inventory_id']
35 | ]
36 | , 'product' => [
37 | 'id' => $p['product_id']
38 | , 'name' => $p['product_name']
39 | , 'sell' => $p['sell']
40 | ]
41 | , 'variety' => $p['variety']
42 | , 'qty' => $_SESSION[$cart][ $p['inventory_id'] ]
43 | ];
44 | }
45 |
46 | $html = $this->render('shop/cart.php', $data);
47 |
48 | return $RES->write($html);
49 |
50 | }
51 |
52 | /**
53 | *
54 | */
55 | function post($REQ, $RES, $ARG)
56 | {
57 | switch ($_POST['a']) {
58 | case 'cart-add':
59 |
60 | $c = $_POST['company-id'];
61 | $i = $_POST['inventory-id'];
62 |
63 | $cart = sprintf('cart-%s', $c);
64 |
65 | if (empty($_SESSION[$cart])) {
66 | $_SESSION[$cart] = [];
67 | }
68 |
69 |
70 | if (empty($_SESSION[$cart][$i])) {
71 | $_SESSION[$cart][$i] = 1;
72 | } else {
73 | $_SESSION[$cart][$i]++;
74 | }
75 |
76 | __exit_json([
77 | 'data' => [
78 | 'count' => $_SESSION[$cart][$i],
79 | ],
80 | 'meta' => [],
81 | ]);
82 |
83 | break;
84 |
85 | case 'cart-continue':
86 |
87 | $dbc_auth = _dbc('auth');
88 | $Company = $dbc_auth->fetchRow('SELECT id, name FROM auth_company WHERE id = :c0', [ ':c0' => $_GET['c'] ]);
89 | if (empty($Company['id'])) {
90 | _exit_html_warn('Invalid Request [CSC-064]', 400);
91 | }
92 |
93 | $cart = sprintf('cart-%s', $Company['id']);
94 |
95 | $b2b_sale = [];
96 | $b2b_sale['id'] = ULID::create();
97 | $b2b_sale['company'] = $Company;
98 |
99 | $product_want_list = $this->load_cart_product_list($Company['id']);
100 | foreach ($product_want_list as $p) {
101 |
102 | $b2b_item = [];
103 | $b2b_item['qty'] = $_SESSION[$cart][$p['inventory_id']];
104 | $b2b_item['inventory'] = [
105 | 'id' => $p['inventory_id']
106 | ];
107 | $b2b_item['product'] = [
108 | 'id' => $p['product_id'],
109 | 'name' => $p['product_name'],
110 | ];
111 | $b2b_item['variety'] = [
112 | 'id' => $p['variety_id'],
113 | 'name' => $p['variety_name']
114 | ];
115 |
116 | $b2b_sale['item_list'][] = $b2b_item;
117 |
118 | }
119 |
120 | $key = sprintf('b2b-sale-%s', $b2b_sale['id']);
121 | $_SESSION[$key] = $b2b_sale;
122 |
123 | return $RES->withRedirect(sprintf('/shop/checkout?o=%s', $b2b_sale['id']));
124 |
125 | break;
126 |
127 | case 'cart-delete':
128 |
129 | $key_list = array_keys($_SESSION);
130 | foreach ($key_list as $k) {
131 | if (preg_match('/^cart/', $k)) {
132 | unset($_SESSION[$k]);
133 | }
134 | }
135 |
136 | \Edoceo\Radix\Session::flash('info', 'Cart has been emptied');
137 |
138 | return $RES->withRedirect(sprintf('/shop?c=%s', $_GET['c']));
139 | }
140 |
141 | }
142 |
143 | /**
144 | *
145 | */
146 | function load_cart_product_list($c)
147 | {
148 | $dbc_auth = _dbc('auth');
149 | $Company = $dbc_auth->fetchRow('SELECT id, name, dsn FROM auth_company WHERE id = :c0', [ ':c0' => $c ]);
150 | if (empty($Company['id'])) {
151 | _exit_html_fail('Invalid Request [CSC-121]', 400);
152 | }
153 |
154 | $dbc_user = _dbc($Company['dsn']);
155 |
156 | $sql = << 0
190 | AND inventory.id IN ({product_id_list})
191 | ORDER BY product_type_name, product_name, package_unit_uom
192 | SQL;
193 |
194 | $cart = sprintf('cart-%s', $c);
195 | $product_want = $_SESSION[$cart];
196 | if (empty($product_want)) {
197 | return [];
198 | }
199 |
200 | $idx = 0;
201 | $arg = [];
202 | $tmp = [];
203 | foreach ($product_want as $p => $q) {
204 | $idx++;
205 | $k = sprintf(':pw%d', $idx);
206 | $tmp[] = $k;
207 | $arg[$k] = $p;
208 | }
209 |
210 | $sql = str_replace('{product_id_list}', implode(',', $tmp), $sql);
211 |
212 | $product_list = $dbc_user->fetchAll($sql, $arg);
213 | return $product_list;
214 |
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/lib/Controller/Shop/Checkout.php:
--------------------------------------------------------------------------------
1 | data;
20 |
21 | $key = sprintf('b2b-sale-%s', $_GET['o']);
22 | $data['b2b_sale'] = $_SESSION[$key];
23 | $data['Page']['title'] = sprintf('Checkout :: %s', $data['b2b_sale']['company']['name']);
24 |
25 | $html = $this->render('shop/checkout.php', $data);
26 |
27 | return $RES->write($html);
28 | }
29 |
30 | /**
31 | *
32 | */
33 | function done($REQ, $RES, $ARG)
34 | {
35 | $data = $this->data;
36 |
37 | // @todo Lookup Company & Order
38 | $dbc_auth = _dbc('auth');
39 | $Company = $dbc_auth->fetchRow('SELECT id, name, dsn FROM auth_company WHERE id = :c0', [ ':c0' => $_GET['c'] ]);
40 | if (empty($Company['id'])) {
41 | _exit_html_fail('Invalid Request [CSC-037]
', 500);
42 | }
43 |
44 | $data['Page']['title'] = sprintf('Checkout :: %s', $Company['name']);
45 |
46 | $dbc_user = _dbc($Company['dsn']);
47 | $rec = $dbc_user->fetchRow('SELECT * FROM b2c_sale_hold WHERE id = :pk', [ ':pk' => $_GET['o'] ]);
48 | $rec['meta'] = json_decode($rec['meta'], true);
49 | $data['b2c'] = $rec['meta'];
50 |
51 | $html = $this->render('shop/checkout-done.php', $data);
52 |
53 | return $RES->write($html);
54 |
55 | }
56 |
57 | /**
58 | *
59 | */
60 | function post($REQ, $RES, $ARG)
61 | {
62 | switch ($_POST['a']) {
63 | case 'cart-commit':
64 |
65 | $_POST['contact-email'] = filter_var($_POST['contact-email'], FILTER_VALIDATE_EMAIL);
66 | if (empty($_POST['contact-email'])) {
67 | _exit_html_warn('Invalid Email Address [CSC-057]
Please go back and correct
', 400);
68 | }
69 | $_POST['contact-phone'] = _phone_e164($_POST['contact-phone']);
70 |
71 |
72 | $b2b = $_SESSION[sprintf('b2b-sale-%s', $_GET['o'])];
73 | $b2b['contact'] = [
74 | 'name' => $_POST['contact-name'],
75 | 'email' => $_POST['contact-email'],
76 | 'phone' => $_POST['contact-phone']
77 | ];
78 |
79 | // Company
80 | $dbc_auth = _dbc('auth');
81 | $Company = $dbc_auth->fetchRow('SELECT id, name, dsn FROM auth_company WHERE id = :c0', [ ':c0' => $b2b['company']['id'] ]);
82 | if (empty($Company['id'])) {
83 | _exit_html_fail('Invalid Request [CSC-063]
', 500);
84 | }
85 |
86 | $dbc_user = _dbc($Company['dsn']);
87 | unset($Company['dsn']);
88 |
89 | // Lookup Contact
90 | $chk = $dbc_user->fetchOne('SELECT id FROM contact WHERE email = :e', [ ':e' => $b2b['contact']['email'] ]);
91 | if (empty($chk)) {
92 | $chk = $dbc_user->fetchOne('SELECT id FROM contact WHERE phone = :p', [ ':p' => $b2b['contact']['phone'] ]);
93 | }
94 | if (empty($chk)) {
95 |
96 | // Search Global Directory?
97 | // $dir = new \OpenTHC\Service\OpenTHC('dir');
98 | // $chk = $dir->get(sprintf('/api/contact/search?q=%s', rawurlencode($_POST['contact-email'])));
99 | $chk = ULID::create();
100 | $rec = [
101 | 'id' => $chk,
102 | 'type' => 'client',
103 | 'fullname' => $b2b['contact']['name'],
104 | 'email' => $b2b['contact']['email'],
105 | 'phone' => $b2b['contact']['phone'],
106 | ];
107 | $rec['hash'] = md5(json_encode($rec));
108 | $rec['guid'] = $rec['id'];
109 | $dbc_user->insert('contact', $rec);
110 |
111 | }
112 |
113 | $Contact = [
114 | 'id' => $chk
115 | ];
116 |
117 | // Insert POS Hold
118 | $dbc_user->insert('b2c_sale_hold', [
119 | 'id' => $b2b['id'],
120 | 'contact_id' => $Contact['id'],
121 | 'type' => 'online',
122 | 'stat' => 100,
123 | 'meta' => json_encode($b2b),
124 | ]);
125 |
126 | unset($_SESSION[sprintf('b2b-sale-%s', $b2b['id'])]);
127 | unset($_SESSION[sprintf('cart-%s', $Company['id'])]);
128 |
129 | break;
130 |
131 | }
132 |
133 | // Redirect
134 | return $RES->withRedirect(sprintf('/shop/checkout/done?c=%s&o=%s'
135 | , $b2b['company']['id']
136 | , $b2b['id']
137 | ));
138 |
139 | }
140 |
141 | }
142 |
--------------------------------------------------------------------------------
/lib/Controller/Shop/Example.php:
--------------------------------------------------------------------------------
1 | data;
17 |
18 | $dbc_auth = _dbc('auth');
19 | $Company = $dbc_auth->fetchRow('SELECT id, name, dsn FROM auth_company WHERE id = :c0', [ ':c0' => $_GET['c'] ]);
20 | if (empty($Company['id'])) {
21 | _exit_html_fail('Invalid Request [CSE-020]', 400);
22 | }
23 |
24 | $dbc_user = _dbc($Company['dsn']);
25 | unset($Company['dsn']);
26 |
27 | $data['Page']['title'] = sprintf('Example Shop :: %s', $Company['name']);
28 | $data['Company'] = $Company;
29 |
30 | $sql = << 0
65 | ORDER BY product_type_name, product_name, package_unit_uom
66 | SQL;
67 | $data['product_list'] = $dbc_user->fetchAll($sql);
68 |
69 | $html = $this->render('shop/example.php', $data);
70 |
71 | return $RES->write($html);
72 |
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/lib/Controller/Shop/Main.php:
--------------------------------------------------------------------------------
1 | data;
14 |
15 | $dbc_auth = _dbc('auth');
16 | $Company = $dbc_auth->fetchRow('SELECT id, name, dsn FROM auth_company WHERE id = :c0', [ ':c0' => $_GET['c'] ]);
17 | if (empty($Company['id'])) {
18 | _exit_html_fail('Invalid Request [CSE-020]', 400);
19 | }
20 |
21 | $dbc_user = _dbc($Company['dsn']);
22 | unset($Company['dsn']);
23 |
24 | $data['Page']['title'] = sprintf('Example Shop :: %s', $Company['name']);
25 | $data['Company'] = $Company;
26 | $data['product_list'] = $this->load_inventory($dbc_user);
27 |
28 | $html = $this->render('shop/example.php', $data);
29 | return $RES->write($html);
30 | }
31 |
32 | /**
33 | *
34 | */
35 | function load_inventory($dbc_user)
36 | {
37 | $sql = << 0
72 | ORDER BY product_type_name, product_name, package_unit_uom
73 | SQL;
74 | $product_list = $dbc_user->fetchAll($sql);
75 |
76 | return $product_list;
77 |
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/lib/Inventory.php:
--------------------------------------------------------------------------------
1 | = :d';
17 | $arg = array(
18 | ':id' => $this->_data['id'],
19 | ':d' => $x
20 | );
21 | $res = $this->_dbc->query($sql, $arg);
22 | if (empty($res)) {
23 | throw new \Exception("Could not Decrement Inventory");
24 | }
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/lib/Middleware/Auth.php:
--------------------------------------------------------------------------------
1 | withRedirect('/auth/open');
18 | }
19 |
20 | $RES = $NMW($REQ, $RES);
21 |
22 | return $RES;
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/lib/Module/API.php:
--------------------------------------------------------------------------------
1 | get('', 'OpenTHC\POS\Controller\API\Main');
17 |
18 | // $a->post('/print', 'OpenTHC\POS\Controller\API\Print');
19 |
20 | // Create Sale
21 | $a->post('/b2c', 'OpenTHC\POS\Controller\API\B2C');
22 |
23 | $a->get('/b2c/receipt/preview', 'OpenTHC\POS\Controller\API\B2C\Receipt:preview');
24 |
25 | $a->get('/b2c/{id}', 'OpenTHC\POS\Controller\API\B2C\Single');
26 |
27 | $a->post('/b2c/{id}', 'OpenTHC\POS\Controller\API\B2C\Single:post');
28 | $a->post('/b2c/{id}/item', 'OpenTHC\POS\Controller\API\B2C\Item');
29 |
30 | $a->post('/b2c/{id}/verify', 'OpenTHC\POS\Controller\API\B2C\Single:verify');
31 | // $a->post('/b2c/{id}/payment', 'OpenTHC\POS\Controller\API\B2C\Single:post');
32 | $a->post('/b2c/{id}/commit', 'OpenTHC\POS\Controller\API\B2C\Single:commit');
33 |
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/lib/Module/B2B.php:
--------------------------------------------------------------------------------
1 | get('', 'OpenTHC\POS\Controller\B2B\Main');
15 |
16 | // $a->get('/incoming');
17 | $a->get('/incoming/create', 'OpenTHC\POS\Controller\B2B\Incoming\Create');
18 | $a->post('/incoming/create', 'OpenTHC\POS\Controller\B2B\Incoming\Create:post');
19 |
20 | $a->map(['GET','POST'], '/sync', 'OpenTHC\POS\Controller\B2B\Sync');
21 | // $a->map(['GET', 'POST'], '/{id}/sync', 'OpenTHC\POS\Controller\B2B\Sync');
22 |
23 |
24 |
25 | $a->get('/{id}', 'OpenTHC\POS\Controller\B2B\View');
26 | $a->post('/{id}', 'OpenTHC\POS\Controller\B2B\View');
27 |
28 | $a->get('/{id}/accept', 'OpenTHC\POS\Controller\B2B\Accept');
29 | $a->post('/{id}/accept', 'OpenTHC\POS\Controller\B2B\Accept');
30 |
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/lib/Module/CRM.php:
--------------------------------------------------------------------------------
1 | get('', 'OpenTHC\POS\Controller\CRM\Main');
15 | $a->get('/contact', 'OpenTHC\POS\Controller\CRM\Contact');
16 | $a->post('/contact', 'OpenTHC\POS\Controller\CRM\Contact:save');
17 | $a->get('/message', 'OpenTHC\POS\Controller\CRM\Message');
18 | $a->get('/message/sms', 'OpenTHC\POS\Controller\CRM\Message:sms');
19 | $a->get('/message/email', 'OpenTHC\POS\Controller\CRM\Message:email');
20 | $a->get('/ajax', 'OpenTHC\POS\Controller\CRM\Ajax');
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/lib/Module/Dashboard.php:
--------------------------------------------------------------------------------
1 | get('', 'OpenTHC\POS\Controller\Dashboard\Main');
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lib/Module/Menu.php:
--------------------------------------------------------------------------------
1 | get('', 'OpenTHC\POS\Controller\Menu\Main');
15 | $a->get('/online', 'OpenTHC\POS\Controller\Menu\Online');
16 | $a->get('/onsite', 'OpenTHC\POS\Controller\Menu\Onsite');
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/lib/Module/POS.php:
--------------------------------------------------------------------------------
1 | get('', 'OpenTHC\POS\Controller\POS\Main');
18 | $a->post('', 'OpenTHC\POS\Controller\POS\Main:post');
19 |
20 | $a->get('/fast', 'OpenTHC\POS\Controller\POS\Fast');
21 | $a->get('/open', function($REQ, $RES, $ARG) {
22 | unset($_SESSION['Cart']);
23 | return $RES->withRedirect('/pos');
24 | });
25 |
26 | $a->map([ 'GET', 'POST', 'DELETE' ], '/ajax', 'OpenTHC\POS\Controller\POS\Ajax');
27 |
28 | $a->post('/checkout/commit', 'OpenTHC\POS\Controller\POS\Checkout\Commit');
29 | $a->get('/checkout/done', 'OpenTHC\POS\Controller\POS\Checkout\Done');
30 |
31 | $a->get('/checkout/open', 'OpenTHC\POS\Controller\POS\Checkout\Open');
32 | $a->post('/checkout/open', 'OpenTHC\POS\Controller\POS\Checkout\Open:post');
33 |
34 | $a->map([ 'GET', 'POST' ], '/checkout/receipt', 'OpenTHC\POS\Controller\POS\Checkout\Receipt');
35 |
36 | $a->post('/cart/ajax', 'OpenTHC\POS\Controller\POS\Cart\Ajax');
37 | $a->post('/cart/drop', 'OpenTHC\POS\Controller\POS\Cart\Drop');
38 | $a->post('/cart/save', 'OpenTHC\POS\Controller\POS\Cart\Save');
39 |
40 | $a->get('/delivery', 'OpenTHC\POS\Controller\POS\Delivery');
41 | $a->map([ 'GET', 'POST' ], '/delivery/ajax', 'OpenTHC\POS\Controller\POS\Delivery:ajax');
42 |
43 | $a->get('/online', 'OpenTHC\POS\Controller\POS\Online');
44 |
45 | $a->get('/shut', 'OpenTHC\POS\Controller\POS\Shut');
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/lib/Module/Report.php:
--------------------------------------------------------------------------------
1 | get('', 'OpenTHC\POS\Controller\Report\Main');
15 | $a->get('/b2c/recent', 'OpenTHC\POS\Controller\Report\Main:recent');
16 | // , function($REQ, $RES, $ARG) {
17 | // return $this->view->render($RES, 'page/report/b2c/recent.html', []);
18 | // });
19 | $a->map(['GET', 'POST'], '/ajax', 'OpenTHC\POS\Controller\Report\Ajax');
20 | $a->get('/ajax/revenue-daily', function($REQ, $RES, $ARG) {
21 | require_once(APP_ROOT . '/lib/Controller/Report/Ajax.php');
22 | });
23 | $a->get('/ajax/revenue-product-type', function($REQ, $RES, $ARG) {
24 | require_once(APP_ROOT . '/lib/Controller/Report/revenue-product-type.php');
25 | });
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/lib/Module/Shop.php:
--------------------------------------------------------------------------------
1 | get('', 'OpenTHC\POS\Controller\Shop\Main');
15 |
16 | $a->get('/cart', 'OpenTHC\POS\Controller\Shop\Cart');
17 | $a->post('/cart', 'OpenTHC\POS\Controller\Shop\Cart:post');
18 |
19 | $a->get('/checkout', 'OpenTHC\POS\Controller\Shop\Checkout');
20 | $a->post('/checkout', 'OpenTHC\POS\Controller\Shop\Checkout:post');
21 |
22 | $a->get('/checkout/done', 'OpenTHC\POS\Controller\Shop\Checkout:done');
23 |
24 | $a->get('/example', 'OpenTHC\POS\Controller\Shop\Example');
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/lib/Module/Webhook.php:
--------------------------------------------------------------------------------
1 | get('', 'OpenTHC\POS\Controller\Webhook\Main');
20 |
21 | $a->post('/weedmaps/order', function($REQ, $RES, $ARG) {
22 |
23 | $file = sprintf('%s/var/weedmaps-order-%s.txt', APP_ROOT, ULID::create());
24 | $json = file_get_contents('php://input');
25 | file_put_contents($file, json_encode([
26 | '_GET' => $_GET,
27 | '_POST' => $_POST,
28 | '_ENV' => $_ENV,
29 | '_BODY' => $json
30 | ]));
31 |
32 | $data = json_decode($json, true);
33 | switch ($data['status']) {
34 | case 'DRAFT':
35 | __exit_json($data);
36 | break;
37 | case 'PENDING':
38 | __exit_json($data);
39 | break;
40 | }
41 |
42 | __exit_json([
43 | 'data' => null,
44 | 'meta' => [ 'note' => 'Request Not Handled' ]
45 | ], 400);
46 |
47 | });
48 |
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib/PDF/Base.php:
--------------------------------------------------------------------------------
1 | setAuthor('openthc.com');
21 | $this->setCreator('openthc.com');
22 | //$this->setTitle($name);
23 | //$this->setSubject($name);
24 | //$this->setKeywords($this->name);
25 |
26 | // set margins
27 | $this->setMargins(0, 0, 0, true);
28 | $this->setHeaderMargin(0);
29 | $this->setPrintHeader(false);
30 | $this->setFooterMargin(0);
31 | $this->setPrintFooter(false);
32 |
33 | // set auto page breaks
34 | $this->setAutoPageBreak(true, 1/2);
35 |
36 | // set image scale factor
37 | $this->setImageScale(0);
38 |
39 | // set default font subsetting mode
40 | $this->setFontSpacing(0);
41 | $this->setFontStretching(100);
42 | $this->setFontSubsetting(true);
43 |
44 | // Cells
45 | $this->setCellMargins(0, 0, 0, 0);
46 | $this->setCellPaddings(0, 0, 0, 0);
47 |
48 | // Set font
49 | $this->setFont('freesans', '', 14, '', true);
50 |
51 | // Set viewer preferences
52 | $arg = array(
53 | 'HideToolbar' => true,
54 | 'HideMenubar' => true,
55 | 'HideWindowUI' => true,
56 | 'FitWindow' => true,
57 | 'CenterWindow' => true,
58 | 'DisplayDocTitle' => true,
59 | 'NonFullScreenPageMode' => 'UseNone', // UseNone, UseOutlines, UseThumbs, UseOC
60 | 'ViewArea' => 'CropBox', // CropBox, BleedBox, TrimBox, ArtBox
61 | 'ViewClip' => 'CropBox', // CropBox, BleedBox, TrimBox, ArtBox
62 | 'PrintArea' => 'CropBox', // CropBox, BleedBox, TrimBox, ArtBox
63 | 'PrintClip' => 'CropBox', // CropBox, BleedBox, TrimBox, ArtBox
64 | 'PrintScaling' => 'None', // None, AppDefault
65 | 'Duplex' => 'Simplex', // Simplex, DuplexFlipShortEdge, DuplexFlipLongEdge
66 | //'PickTrayByPDFSize' => true,
67 | //'PrintPageRange' => array(1,1,2,3),
68 | //'NumCopies' => 2
69 | );
70 | $this->setViewerPreferences($arg);
71 | $this->SetDisplayMode('fullwidth');
72 |
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/lib/Product.php:
--------------------------------------------------------------------------------
1 | _public = $cfg['public'];
19 | $this->_secret = $cfg['secret'];
20 | }
21 |
22 | function getAuthToken()
23 | {
24 | $req = _curl_init('https://api-g.weedmaps.com/auth/token');
25 |
26 | $req_body = json_encode([
27 | 'client_id' => $this->_public,
28 | 'client_secret' => $this->_secret,
29 | 'grant_type' => 'client_credentials',
30 | 'scope' => 'taxonomy:read brands:read products:read menu_items menus:write',
31 | ]);
32 | curl_setopt($req, CURLOPT_POST, true);
33 | curl_setopt($req, CURLOPT_POSTFIELDS, $req_body);
34 |
35 | $req_head = [];
36 | $req_head[] = 'accept: application/json';
37 | $req_head[] = 'content-type: application/json';
38 | // $req_head[] =
39 |
40 | curl_setopt($req, CURLOPT_HTTPHEADER, $req_head);
41 |
42 | $res = curl_exec($req);
43 | $res = json_decode($res, true);
44 |
45 | if (!empty($res['created_at']) && !empty($res['expires_in'])) {
46 | $res['expires_at'] = $res['created_at'] + $res['expires_in'];
47 | $res['expires_at_iso'] = date(\DateTime::ISO8601, $res['expires_at']);
48 | }
49 |
50 | $this->setAuthToken($res['access_token']);
51 |
52 | return $res;
53 |
54 | }
55 |
56 | function setAuthToken($t)
57 | {
58 | $this->_auth_token = $t;
59 | }
60 |
61 | /**
62 | *
63 | */
64 | function get($path)
65 | {
66 | $url = sprintf('https://api-g.weedmaps.com%s', $path);
67 |
68 | $req = _curl_init($url);
69 |
70 | $req_head = [];
71 | $req_head[] = 'accept: application/json';
72 | $req_head[] = sprintf('authorization: Bearer %s', $this->_auth_token);
73 | curl_setopt($req, CURLOPT_HTTPHEADER, $req_head);
74 |
75 | $res = curl_exec($req);
76 | $res = json_decode($res, true);
77 |
78 | return $res;
79 |
80 | }
81 |
82 | /**
83 | *
84 | */
85 | function post_form($path, $body)
86 | {
87 | $url = sprintf('https://api-g.weedmaps.com%s', $path);
88 | $req = _curl_init($url);
89 | curl_setopt($req, CURLOPT_POST, true);
90 | curl_setopt($req, CURLOPT_POSTFIELDS, $body);
91 |
92 | $req_head = [];
93 | $req_head[] = 'accept: application/json';
94 | $req_head[] = sprintf('authorization: Bearer %s', $this->_auth_token);
95 | $req_head[] = 'content-type: application/x-www-form-urlencoded';
96 | curl_setopt($req, CURLOPT_HTTPHEADER, $req_head);
97 |
98 | $res = curl_exec($req);
99 | $res = json_decode($res, true);
100 |
101 | return $res;
102 |
103 | }
104 |
105 | /**
106 | *
107 | */
108 | function post_json($path, $body)
109 | {
110 | $url = sprintf('https://api-g.weedmaps.com%s', $path);
111 | $req = _curl_init($url);
112 | curl_setopt($req, CURLOPT_POST, true);
113 | curl_setopt($req, CURLOPT_POSTFIELDS, $body);
114 |
115 | $req_head = [];
116 | $req_head[] = 'accept: application/json';
117 | $req_head[] = sprintf('authorization: Bearer %s', $this->_auth_token);
118 | $req_head[] = 'content-type: application/json';
119 | curl_setopt($req, CURLOPT_HTTPHEADER, $req_head);
120 |
121 | $res = curl_exec($req);
122 | $res = json_decode($res, true);
123 |
124 | return $res;
125 |
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/lib/Traits/OpenAuthBox.php:
--------------------------------------------------------------------------------
1 | pk, $cpk) !== 0) {
24 | throw new \Exception('Authentication Box Invalid Service Key [PCB-032]', 403);
25 | }
26 |
27 | // Time Check
28 | $dt0 = new \DateTime();
29 | $dt1 = \DateTime::createFromFormat('U', $act->ts);
30 | $age = $dt0->diff($dt1, true);
31 | if (($age->d != 0) || ($age->h != 0) || ($age->i > 5)) {
32 | throw new \Exception('Authentication Box Expired [PCB-040]', 400);
33 | }
34 |
35 | if (empty($act->contact)) {
36 | throw new \Exception('Authentication Box Data Corrupted [PCB-103]', 403);
37 | }
38 |
39 | if (empty($act->company)) {
40 | throw new \Exception('Authentication Box Data Corrupted [PCB-110]', 403);
41 | }
42 |
43 | return $act;
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/make.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Install Helper
4 | #
5 | # SPDX-License-Identifier: GPL-3.0-only
6 | #
7 |
8 | set -o errexit
9 | set -o errtrace
10 | set -o nounset
11 | set -o pipefail
12 |
13 | APP_ROOT=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
14 |
15 | cd "$APP_ROOT"
16 |
17 | composer install --no-ansi --no-progress --classmap-authoritative
18 |
19 | npm install --no-audit --no-fund
20 |
21 | php <&1;
57 |
58 | #
59 | #
60 | php <client = $this->getGuzzleClient([
16 | 'base_uri' => OPENTHC_TEST_ORIGIN
17 | ]);
18 | }
19 |
20 | function makeBearerToken()
21 | {
22 | // Client Public Key
23 | $cpk = \OpenTHC\Config::get('openthc/pos/public');
24 | // Client Secret Key
25 | $csk = \OpenTHC\Config::get('openthc/pos/secret');
26 | // Server Public Key (same as Client when requesting to self)
27 | $spk = \OpenTHC\Config::get('openthc/pos/public');
28 |
29 | // Crypto Box of Data
30 | $box = json_encode([
31 | 'pk' => $cpk,
32 | 'ts' => time(),
33 | 'contact' => '',
34 | 'company' => '',
35 | 'license' => '',
36 | ]);
37 | $box = \OpenTHC\Sodium::encrypt($box, $csk, $spk);
38 | $box = \OpenTHC\Sodium::b64encode($box);
39 |
40 | return sprintf('Bearer v2024/%s/%s', $cpk, $box);
41 | }
42 |
43 | /**
44 | HTTP Utility
45 | */
46 | function get($url)
47 | {
48 | $res = $this->ghc->get($url);
49 | $ret = $this->assertValidResponse($res, 200);
50 | return $ret;
51 | }
52 |
53 |
54 | /**
55 | HTTP Utility
56 | */
57 | function post($url, $arg)
58 | {
59 | $res = $this->ghc->post($url, array('json' => $arg));
60 | return $res;
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/test/Core/Config_Test.php:
--------------------------------------------------------------------------------
1 | assertTrue(is_file($f), 'No Config File');
14 | }
15 |
16 | function test_service_config()
17 | {
18 | $cfg = \OpenTHC\Config::get('');
19 | // var_dump($cfg);
20 |
21 | $this->assertIsArray($cfg);
22 |
23 | $key_list = [
24 | 'database/auth/hostname',
25 | 'database/auth/username',
26 | 'database/auth/password',
27 | 'database/auth/database',
28 |
29 | 'database/main/hostname',
30 | 'database/main/username',
31 | 'database/main/password',
32 | 'database/main/database',
33 |
34 | 'redis/hostname',
35 |
36 | 'statsd/hostname',
37 |
38 | // 'metabase/hostname',
39 | // 'metabase/username',
40 | // 'metabase/password',
41 | // 'metabase/embedkey',
42 |
43 | 'openthc/dir/origin',
44 | 'openthc/dir/public',
45 |
46 | 'openthc/pos/origin',
47 | 'openthc/pos/public',
48 | 'openthc/pos/secret',
49 | 'openthc/pos/client-id',
50 | 'openthc/pos/client-sk',
51 |
52 | 'openthc/b2b/origin', // Menu New Name
53 |
54 | // 'openthc/ops/origin',
55 | // 'openthc/ops/public',
56 | // 'openthc/ops/secret',
57 |
58 | 'openthc/sso/origin',
59 | 'openthc/sso/public',
60 |
61 | ];
62 |
63 | foreach ($key_list as $k) {
64 | $v = \OpenTHC\Config::get($k);
65 | $this->assertNotEmpty($v, sprintf('Config %s is missing', $k));
66 | }
67 |
68 | $key_list = [
69 | 'openthc/app/secret',
70 | 'openthc/b2b/secret',
71 | 'openthc/cre/secret',
72 | 'openthc/dir/secret',
73 | 'openthc/lab/secret',
74 | 'openthc/pipe/secret',
75 | 'openthc/sso/secret',
76 | ];
77 |
78 | foreach ($key_list as $k) {
79 | $v = \OpenTHC\Config::get($k);
80 | $this->assertEmpty($v, sprintf('Config %s should be empty/unset', $k));
81 | }
82 |
83 | }
84 |
85 | /**
86 | *
87 | */
88 | function test_api()
89 | {
90 | $cfg = \OpenTHC\Config::get('openthc/pos');
91 | $this->assertIsArray($cfg);
92 |
93 | $this->assertArrayHasKey('client-id', $cfg);
94 | $this->assertNotEmpty($cfg['client-id']);
95 |
96 | $this->assertArrayHasKey('client-pk', $cfg);
97 | $this->assertNotEmpty($cfg['client-pk']);
98 |
99 | $this->assertArrayHasKey('client-sk', $cfg);
100 | $this->assertNotEmpty($cfg['client-sk']);
101 |
102 | }
103 |
104 | /**
105 | *
106 | */
107 | function test_redis()
108 | {
109 | // @todo Check for Redis Config and try to connect
110 | $h = \OpenTHC\Config::get('redis/hostname');
111 | $this->assertNotEmpty($h);
112 |
113 | $k = _random_hash();
114 | $r = new \Redis();
115 | $r->connect($h);
116 | $r->set($k, 'TEST');
117 |
118 | $x = $r->get($k);
119 | $this->assertEquals('TEST', $x);
120 |
121 | }
122 |
123 | function test_statsd()
124 | {
125 | // @todo Check for Config & functions?
126 | $h = \OpenTHC\Config::get('statsd/hostname');
127 | $this->assertNotEmpty($h);
128 |
129 | $this->assertTrue(function_exists('_stat_counter'));
130 | $this->assertTrue(function_exists('_stat_gauge'));
131 | $this->assertTrue(function_exists('_stat_timer'));
132 |
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/test/Core/Core_Test.php:
--------------------------------------------------------------------------------
1 | assertTrue(is_dir($dir));
18 |
19 | // Var Path
20 | $var = sprintf('%s/var', APP_ROOT);
21 | $this->assertTrue(is_dir($var));
22 |
23 | $var_stat = stat($var);
24 |
25 | $o = posix_getpwuid($var_stat[4]);
26 | $this->assertIsArray($o);
27 | $this->assertEquals('openthc', $o['name']);
28 |
29 | $g = posix_getgrgid($var_stat[5]);
30 | $this->assertIsArray($g);
31 | $this->assertEquals('www-data', $g['name']);
32 |
33 | $m = ($var_stat[2] & 0x0fff);
34 | $this->assertEquals($m, 0775); // Perms in OCTAL
35 |
36 | // Webroot Output Path
37 | $dir = sprintf('%s/webroot/output', APP_ROOT);
38 | $dir_stat = stat($dir);
39 |
40 | $o = posix_getpwuid($dir_stat[4]);
41 | $this->assertIsArray($o);
42 | $this->assertEquals('openthc', $o['name']);
43 |
44 | $g = posix_getgrgid($dir_stat[5]);
45 | $this->assertIsArray($g);
46 | $this->assertEquals('www-data', $g['name']);
47 |
48 | $m = ($dir_stat[2] & 0x0fff);
49 | $this->assertEquals($m, 0775); // Perms in OCTAL
50 |
51 | // @todo Check all the other directories/files to be owned by 'openthc'
52 |
53 | // $this->assertIsTrue();
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/test/Core/System_Test.php:
--------------------------------------------------------------------------------
1 | assertTrue(is_file($f), '/usr/bin/convert not found');
14 | $this->assertTrue(is_executable($f), '/usr/bin/convert not executable');
15 | }
16 |
17 | function test_convert_policy()
18 | {
19 | // Read the Convert Policy XML File
20 | // Make sure that PS and PDF *ARE* Allowed
21 | $xml_file = '/etc/ImageMagick-6/policy.xml';
22 | $this->assertTrue(is_file($xml_file), 'No File');
23 | $xml_data = simplexml_load_file($xml_file);
24 | // Do the tright thing here
25 | }
26 |
27 | function test_exiftool()
28 | {
29 | $f = '/usr/bin/exiftool';
30 | $this->assertTrue(is_file($f));
31 | $this->assertTrue(is_executable($f));
32 | }
33 |
34 |
35 | function test_gs()
36 | {
37 | $f = '/usr/bin/gs';
38 | $this->assertTrue(is_file($f), '/usr/bin/gs does not exist');
39 | $this->assertTrue(is_executable($f), '/usr/bin/gs is not executable');
40 | }
41 |
42 | function test_pdfunite()
43 | {
44 | $f = '/usr/bin/pdfunite';
45 | $this->assertTrue(is_file($f));
46 | $this->assertTrue(is_executable($f));
47 | }
48 |
49 | // function test_puppeteer()
50 | // {
51 | // $f = '/usr/bin/node';
52 | // $this->assertTrue(is_file($f), '/usr/bin/node does not exist');
53 | // $this->assertTrue(is_executable($f), '/usr/bin/node is not Executable');
54 |
55 | // $f = sprintf('%s/node_modules/puppeteer/index.js', APP_ROOT);
56 | // $this->assertTrue(is_file($f), 'puppeteer/index.js does not exist');
57 | // }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/test/Unit/API_Test.php:
--------------------------------------------------------------------------------
1 | [
16 | 'Authorization' => $this->makeBearerToken(),
17 | 'openthc-contact-id' => '',
18 | 'openthc-company-id' => '',
19 | 'openthc-license-id' => '',
20 | ],
21 | 'json' => [
22 | 'id' => '',
23 | 'created_at' => '',
24 | ]
25 | ];
26 | $res = $this->client->post('/api/v2018/b2c', $arg);
27 | $res = $this->assertValidResponse($res);
28 |
29 | var_dump($res);
30 |
31 | return $b2c_sale;
32 | }
33 |
34 | /**
35 | * @depends test_b2c_sale_create
36 | */
37 | function test_b2c_sale_item_create($b2c_sale)
38 | {
39 | $url = sprintf('/api/v2018/b2c/%s/item', $b2c_sale->id);
40 | $arg = [
41 | 'headers' => [
42 | 'Authorization' => $this->makeBearerToken(),
43 | ],
44 | 'json' => [
45 | 'inventory' => [ 'id' => '' ],
46 | 'unit_count' => 1,
47 | 'unit_price' => 2,
48 | ]
49 | ];
50 |
51 | $res = $this->client->post($url, $arg);
52 | $res = $this->assertValidResponse($res);
53 |
54 | var_dump($res);
55 |
56 | return $b2c_sale;
57 | }
58 |
59 | /**
60 | * @depends test_b2c_sale_item_create
61 | */
62 | function test_b2c_sale_detail($b2c_sale)
63 | {
64 | $url = sprintf('/api/v2018/b2c/%s', $b2c_sale->id);
65 | $arg = [
66 | 'headers' => [
67 | 'Authorization' => $this->makeBearerToken(),
68 | ],
69 | ];
70 | $res = $this->client->get($url);
71 | $res = $this->assertValidResponse($res);
72 |
73 | var_dump($res);
74 |
75 | return $b2c_sale;
76 | }
77 |
78 | /**
79 | * @depends test_b2c_sale_detail
80 | */
81 | function test_b2c_sale_verify($b2c_sale)
82 | {
83 | $url = sprintf('/api/v2018/b2c/%s/verify', $b2c_sale->id);
84 | $arg = [
85 | 'headers' => [
86 | 'Authorization' => $this->makeBearerToken(),
87 | ],
88 | ];
89 | $res = $this->client->post($url);
90 | $res = $this->assertValidResponse($res);
91 |
92 | return $b2c_sale;
93 | }
94 |
95 | /**
96 | * @depends test_b2c_sale_verify
97 | */
98 | function test_b2c_sale_commit($b2c_sale)
99 | {
100 | $url = sprintf('/api/v2018/b2c/%s/commit', $b2c_sale->id);
101 | $arg = [
102 | 'headers' => [
103 | 'Authorization' => $this->makeBearerToken(),
104 | ],
105 | 'json' => [
106 | 'key' => 'val',
107 | ]
108 | ];
109 | $res = $this->client->post($url);
110 | $res = $this->assertValidResponse($res);
111 |
112 | return $b2c_sale;
113 | }
114 |
115 | /**
116 | * @depends test_b2c_sale_commit
117 | */
118 | function test_b2c_sale_commit_fail($b2c_sale)
119 | {
120 | $url = sprintf('/api/v2018/b2c/%s/commit', $b2c_sale->id);
121 | $arg = [
122 | 'headers' => [
123 | 'Authorization' => $this->makeBearerToken(),
124 | ],
125 | 'json' => [
126 | 'key' => 'val',
127 | ]
128 | ];
129 | $res = $this->client->post($url);
130 | $res = $this->assertValidResponse($res, 409);
131 |
132 | return $b2c_sale;
133 |
134 | }
135 |
136 | }
137 |
138 | # -X POST \
139 | # --header 'content-type: application/json' \
140 | # --data '{ "Email": "npc@nationalpaymentcard.com", "MerchantID": "TEST", "PIN": "1234", "ReturnPseudoCardNumber": true }'
141 |
142 | #Response:-
143 | # {
144 | #"data": null,
145 | #"meta": {
146 | #"detail": "API query '/api/v2017/b2c/commit' was not understood [CA7#024]"
147 | #}
148 | #}
149 |
150 | # Create B2C Sale Record
151 | # curl 'https://app.djb.openthc.dev/api/v2017/b2c' \
152 | # -X 'POST' \
153 | # --header 'authorization: Bearer uEu72zR6brePjYdt6J2pIBzf5SfcJ9K4ccz0Q1AELI65EAW88QbBuFOXDUUEA_g7' \
154 | # --header 'content-type: application/json' \
155 | # --data '{ "license": { "id": "$OPENTHC_LICENSE" } }'
156 |
157 | # # Add Item to B2C Sale Record
158 | # curl 'https://app.djb.openthc.dev/api/v2017/b2c/item' \
159 | # -X 'POST' \
160 | # --header 'authorization: Bearer uEu72zR6brePjYdt6J2pIBzf5SfcJ9K4ccz0Q1AELI65EAW88QbBuFOXDUUEA_g7' \
161 | # --header 'content-type: application/json' \
162 | # --data '{ "license": { "id": "$OPENTHC_LICENSE" } , "b2c": { "id": $B2C_SALE_ID }, "inventory": { "id": "01FCH7F3AADC2BENAM4V7JBT8Q" }, "unit_count": 5, "unit_price": 5 }'
163 |
164 | # # Commit
165 | # curl 'https://app.djb.openthc.dev/api/v2017/b2c/commit' \
166 | # -X 'POST' \
167 | # --header 'authorization: Bearer uEu72zR6brePjYdt6J2pIBzf5SfcJ9K4ccz0Q1AELI65EAW88QbBuFOXDUUEA_g7' \
168 | # --header 'content-type: application/json' \
169 | # --data '{ "license": { "id": "$OPENTHC_LICENSE" } , "b2c": { "id": $B2C_SALE_ID } }'
170 |
--------------------------------------------------------------------------------
/test/Webhook/Weedmaps_Test.php:
--------------------------------------------------------------------------------
1 | client->post('/webhook/weedmaps/order', [ 'json' => [
13 | 'status' => 'PENDING',
14 | ]]);
15 | $this->assertValidResponse($res);
16 | }
17 |
18 | function test_pending()
19 | {
20 | $res = $this->client->post('/webhook/weedmaps/order', [ 'json' => [
21 | 'status' => 'PENDING',
22 | ]]);
23 | $this->assertValidResponse($res);
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/test/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
5 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | .
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/test/test.php:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | [options]
23 | test phpunit
24 | test phpstan
25 | test phplint
26 |
27 | Options:
28 | --phpunit-filter= Some Filter for PHPUnit
29 | --phpunit-filter= Some Filter for PHPUnit
30 | DOC;
31 |
32 | $res = \Docopt::handle($doc, [
33 | 'exit' => false,
34 | 'optionsFirst' => false,
35 | ]);
36 | $cli_args = $res->args;
37 | // if (empty($cli_args)) {
38 | // echo $res->output;
39 | // echo "\n";
40 | // exit(1);
41 | // }
42 | // var_dump($cli_args);
43 |
44 |
45 | // Test Config
46 | $cfg = [];
47 | $cfg['base'] = APP_ROOT;
48 | $cfg['site'] = 'pos';
49 |
50 | $test_helper = new \OpenTHC\Test\Helper($cfg);
51 | $cfg['output'] = $test_helper->output_path;
52 |
53 |
54 | // PHPLint
55 | if ($cli_args['phplint']) {
56 | $tc = new \OpenTHC\Test\Facade\PHPLint($cfg);
57 | $res = $tc->execute();
58 | var_dump($res);
59 | }
60 |
61 |
62 | // PHPStan
63 | if ($cli_args['phpstan']) {
64 | $tc = new \OpenTHC\Test\Facade\PHPStan($cfg);
65 | $res = $tc->execute();
66 | var_dump($res);
67 | }
68 |
69 |
70 | // Psalm/Psalter?
71 |
72 |
73 | // PHPUnit
74 | $cfg_file_list = [];
75 | $cfg_file_list[] = sprintf('%s/phpunit.xml', __DIR__);
76 | $cfg_file_list[] = sprintf('%s/phpunit.xml.dist', __DIR__);
77 | foreach ($cfg_file_list as $f) {
78 | if (is_file($f)) {
79 | $cfg['--configuration'] = $f;
80 | break;
81 | }
82 | }
83 | // Filter?
84 | if ( ! empty($cli_args['--filter'])) {
85 | $cfg['--filter'] = $cli_args['--filter'];
86 | }
87 | $tc = new \OpenTHC\Test\Facade\PHPUnit($cfg);
88 | $res = $tc->execute();
89 | var_dump($res);
90 |
91 |
92 | // Output
93 | $res = $test_helper->index_create($res['data']);
94 | echo "TEST COMPLETE\n $res\n";
95 |
--------------------------------------------------------------------------------
/view/_block/body-head.php:
--------------------------------------------------------------------------------
1 |
11 |
12 |
43 |
--------------------------------------------------------------------------------
/view/_block/inventory-list.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Inventory ID |
5 | Product |
6 | Package |
7 | QTY |
8 | $/ea |
9 |
10 |
11 |
12 |
15 |
16 | = $inv['guid'] ?> |
17 | = $inv['product_name'] ?> |
18 | = $inv['package_unit_qom'] ?> = $inv['package_unit_uom'] ?> |
19 | = $inv['qty'] ?> |
20 | = $inv['unit_price'] ?> |
21 |
22 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/view/_block/modal.php:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
18 |
19 |
= $data['body'] ?>
20 |
21 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/view/_block/modal/pos/card-swipe.php:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
Swipe Card
11 |
12 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
38 |
39 | echo $this->block('modal.php', [
40 | 'modal_id' => 'pos-modal-checkout-card-swipe',
41 | 'modal_title' => 'Scan ID',
42 | 'body' => $body,
43 | 'foot' => $foot,
44 | ]);
45 |
--------------------------------------------------------------------------------
/view/_block/modal/pos/cart-options.php:
--------------------------------------------------------------------------------
1 |
10 |
11 |
Transaction Date:
12 |
Enter the Date to Register this Transaction
13 |
14 |
20 |
21 |
22 | HTML;
23 |
24 | $foot = << Apply
30 |
31 | HTML;
32 |
33 | echo $this->block('modal.php', [
34 | 'modal_id' => 'pos-modal-cart-option',
35 | 'modal_title' => 'Cart :: Options',
36 | 'body' => $body,
37 | 'foot' => $foot,
38 | ]);
39 |
--------------------------------------------------------------------------------
/view/_block/modal/pos/customer-info.php:
--------------------------------------------------------------------------------
1 |
13 |
23 |
40 | Save
41 |
42 | HTML;
43 |
44 | echo $this->block('modal.php', [
45 | 'modal_id' => 'pos-modal-customer-info',
46 | 'modal_title' => 'Cart :: Customer Information',
47 | 'body' => $body,
48 | 'foot' => $foot,
49 | ]);
50 |
--------------------------------------------------------------------------------
/view/_block/modal/pos/discount.php:
--------------------------------------------------------------------------------
1 |
10 | Fixed Discount
11 |
17 |
18 |
19 |
Percent Discount
20 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
Balance:
34 |
35 |
36 |
37 |
38 |
39 |
Applied Discount
40 |
41 |
-
42 |
43 |
44 |
45 |
New Balance
46 |
47 |
-
48 |
49 |
50 |
51 |
61 | HTML;
62 |
63 |
64 | $foot = <<
69 | Apply
70 |
71 | HTML;
72 |
73 | echo $this->block('modal.php', [
74 | 'modal_id' => 'pos-modal-discount',
75 | 'modal_title' => 'Checkout :: Discount',
76 | 'body' => $body,
77 | 'foot' => $foot,
78 | ]);
79 |
--------------------------------------------------------------------------------
/view/_block/modal/pos/hold.php:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 | HTML;
17 |
18 |
19 | $foot = <<
27 | Save
28 |
29 | HTML;
30 |
31 |
32 | echo $this->block('modal.php', [
33 | 'modal_id' => 'pos-modal-sale-hold',
34 | 'modal_title' => 'Checkout :: Create Hold',
35 | 'body' => $body,
36 | 'foot' => $foot,
37 | ]);
38 |
39 | ?>
40 |
41 |
53 |
--------------------------------------------------------------------------------
/view/_block/modal/pos/keypad.php:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openthc/pos/d54b19901e8d4e075a7a8802eaca36e1bf9ef7f6/view/_block/modal/pos/keypad.php
--------------------------------------------------------------------------------
/view/_block/modal/pos/loyalty.php:
--------------------------------------------------------------------------------
1 |
12 |
13 |
Code / ID
14 |
Their Loyalty ID or scanned ID card
15 |
16 |
22 |
23 |
24 |
25 | Loading...
26 |
27 |
28 | HTML;
29 |
30 | $foot = << Apply
32 | HTML;
33 |
34 |
35 | echo $this->block('modal.php', [
36 | 'modal_id' => 'pos-modal-loyalty',
37 | 'modal_title' => 'Checkout :: Loyalty',
38 | 'body' => $body,
39 | 'foot' => $foot,
40 | ]);
41 |
--------------------------------------------------------------------------------
/view/_block/modal/pos/payment-card.php:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
Sub-Total: $-.--
13 |
14 |
15 |
Taxes: $-.--
16 |
17 |
18 |
Total Due: $
19 |
20 |
21 |
22 |
23 |
37 |
38 |
39 |
40 |
Paid:
41 |
42 |
43 |
44 |
45 |
46 |
Due:
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
Change: $0.00
55 |
56 |
57 |
58 | HTML;
59 |
60 |
61 | $foot = << Undo
63 |
64 | HTML;
65 |
66 |
67 | echo $this->block('modal.php', [
68 | 'modal_id' => 'pos-modal-payment',
69 | 'modal_title' => 'Checkout :: Payment :: Card',
70 | 'body' => $body,
71 | 'foot' => $foot,
72 | ]);
73 |
--------------------------------------------------------------------------------
/view/_block/modal/pos/payment-cash.php:
--------------------------------------------------------------------------------
1 |
10 |
11 |
Sub-Total: $-.--
12 |
13 |
14 |
Taxes: $-.--
15 |
16 |
17 |
Total Due: $
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
Paid:
63 |
0.00
64 |
65 |
66 |
67 |
75 |
76 | HTML;
77 |
78 |
79 | $foot = << Undo
81 |
84 | HTML;
85 |
86 |
87 | echo $this->block('modal.php', [
88 | 'modal_id' => 'pos-modal-payment',
89 | 'modal_title' => 'Checkout :: Payment :: Cash',
90 | 'body' => $body,
91 | 'foot' => $foot,
92 | ]);
93 |
--------------------------------------------------------------------------------
/view/_block/modal/pos/scan-id.php:
--------------------------------------------------------------------------------
1 |
10 |
11 | HTML;
12 |
13 | $foot = << Complete
15 | HTML;
16 |
17 | echo $this->block('modal.php', [
18 | 'modal_id' => 'pos-modal-scan-id',
19 | 'modal_title' => 'Scan ID',
20 | 'body' => $body,
21 | 'foot' => $foot,
22 | ]);
23 |
--------------------------------------------------------------------------------
/view/_block/modal/pos/transaction-limit.php:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
Transaction Over Limits
11 |
12 |
13 |
14 |
According to WAC 314-55-095 there are transaction limitations.
15 |
16 | - One ounce (1oz, 28g) of usable marijuana
17 | - Sixteen ounces (16oz, 453g) of marijuana-infused product in solid form
18 | - Seventy-two ounces (72 floz, ~2L) of marijuana-infused product in liquid form
19 | - Seven grams (7g) of marijuana-infused extract for inhalation
20 |
21 |
22 |
23 |
31 |
32 |
33 |
34 |
35 |
54 |
--------------------------------------------------------------------------------
/view/_block/session-flash.php:
--------------------------------------------------------------------------------
1 | %s
', $x);
16 | }
17 |
--------------------------------------------------------------------------------
/view/_layout/html-pos.php:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | = __h(strip_tags($this->data['Page']['title'])) ?>
24 |
25 |
26 |
50 |
51 | = $this->block('session-flash.php') ?>
52 |
53 | = $this->body ?>
54 |
55 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
95 | = $this->foot_script ?>
96 |
97 |
98 |
--------------------------------------------------------------------------------
/view/_layout/html.php:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | = __h(strip_tags($this->data['Page']['title'])) ?>
27 |
28 |
29 | = $this->block('body-head.php') ?>
30 | = $this->block('session-flash.php') ?>
31 |
32 | = $this->body ?>
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
55 | = $this->foot_script ?>
56 |
57 |
58 |
--------------------------------------------------------------------------------
/view/_layout/shop-html.php:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | = __h(strip_tags($this->data['Page']['title'])) ?>
25 |
26 |
27 |
28 | = $this->block('session-flash.php') ?>
29 |
30 | = $this->body ?>
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | = $this->foot_script ?>
39 |
40 |
41 |
--------------------------------------------------------------------------------
/view/contact/update.php:
--------------------------------------------------------------------------------
1 |
18 |
19 |
36 |
37 |
58 |
--------------------------------------------------------------------------------
/view/crm/contact.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Customer Contact List
7 |
8 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/view/crm/main.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | = _draw_html_card('
Contacts', "View your Contacts, once Contacts are added to this program, they will appear here.", '
Manage') ?>
9 |
10 |
11 | = _draw_html_card(' Messaging', "Send message to one or more Contacts, either via email or text/sms message.", '
Messaging') ?>
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/view/crm/message-compose-email.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
= $data['Page']['title'] ?>
7 |
8 | To send proper emails, use an external HTML & TEXT formatting tool and paste that contents here.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/view/crm/message-compose-sms.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
= $data['Page']['title'] ?>
7 |
8 |
Input the text base messages, and attach messages.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/view/crm/message.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Messaging
7 |
8 |
9 |
10 | = _draw_html_card("Text Blast", "Send a mass text to all of your customers, or filter the customer list.", '
Send Texts') ?>
11 |
12 |
13 | = _draw_html_card("Email Blast", "Mass emails, for monthly specials or large announcments.", '
Send Emails') ?>
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/view/dashboard.php:
--------------------------------------------------------------------------------
1 |
9 |
10 | = __h($_SESSION['License']['name']) ?> :: = __h($_SESSION['License']['code']) ?>
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Retail Sales
19 |
20 |
21 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | Delivery
35 |
36 |
Delivery Management
37 |
38 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | On-Line
52 |
53 |
Online Sales
54 |
55 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/view/done.php:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | %s
', h($data['fail']));
19 | }
20 |
21 | if ($data['warn']) {
22 | printf('
%s
', h($data['warn']));
23 | }
24 |
25 | if ($data['info']) {
26 | printf('
%s
', h($data['info']));
27 | }
28 |
29 | echo $data['body']
30 |
31 | ?>
32 |
33 |
34 | %s
', $data['foot']);
37 | }
38 | ?>
39 |
40 |
41 |
--------------------------------------------------------------------------------
/view/intent/delivery-auth.php:
--------------------------------------------------------------------------------
1 | data['Page']['title'] = 'Delivery Staff Authentication';
7 |
8 | if (empty($_COOKIE['pos-contact'])) {
9 | // Prompt for Username
10 | }
11 |
12 | require_once(sprintf('%s/view/pos/open.php', APP_ROOT));
13 |
--------------------------------------------------------------------------------
/view/intent/main.php:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openthc/pos/d54b19901e8d4e075a7a8802eaca36e1bf9ef7f6/view/intent/main.php
--------------------------------------------------------------------------------
/view/intent/vendor-view.php:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/view/pos/checkout/done.php:
--------------------------------------------------------------------------------
1 | layout_file = sprintf('%s/view/_layout/html-pos.php', APP_ROOT);
7 |
8 | ?>
9 |
10 |
13 |
--------------------------------------------------------------------------------
/view/pos/contact-select.php:
--------------------------------------------------------------------------------
1 | layout_file = sprintf('%s/view/_layout/html-pos.php', APP_ROOT);
9 |
10 | $head = sprintf('%s
', _('Check In:'));
11 |
12 | $govt_id_html = __h($_SESSION['Cart']['contact-search']);
13 |
14 | $body = <<Scan the client's identification card or input their identification details.
17 |
18 |
27 |
28 |
56 |
57 | HTML;
58 |
59 | $foot = [];
60 | $foot[] = '';
61 | $foot[] = '
';
62 |
63 | $foot[] = '';
64 |
65 | if ($_SESSION['Cart']['contact-push']) {
66 | unset($_SESSION['Cart']['contact-push']);
67 | $foot[] = '';
68 | }
69 |
70 | $foot[] = '';
71 |
72 | $foot[] = '
';
73 | $foot[] = '
';
74 | $foot[] ='';
75 | $foot[] = '
';
76 | $foot[] = '
';
77 | $foot = implode(' ', $foot);
78 |
79 | ?>
80 |
81 |
86 |
87 |
232 |
--------------------------------------------------------------------------------
/view/pos/contact-verify.php:
--------------------------------------------------------------------------------
1 | layout_file = sprintf('%s/view/_layout/html-pos.php', APP_ROOT);
9 |
10 | $head = sprintf('%s
', _('Client Contact Verify:'));
11 |
12 | ob_start();
13 | ?>
14 |
15 |
26 |
27 |
28 |
38 |
39 |
40 |
50 |
51 |
52 |
62 |
63 |
64 |
74 |
75 |
76 |
87 |
88 | ';
102 | $foot[] = '';
103 | $foot[] = '';
104 | // $foot[] = '';
105 | // $foot[] = '';
106 | $foot[] = '
';
107 | // $foot[] = '';
108 | // $foot[] ='';
109 | // $foot[] = '
';
110 | $foot[] = '';
111 | $foot = implode(' ', $foot);
112 |
113 | ?>
114 |
115 |
120 |
121 |
127 |
137 |
3 |
4 |
POS Delivery
5 |
6 | 'delivery-auth',
11 | 'c' => $_SESSION['Company']['id'],
12 | 'l' => $_SESSION['License']['id']
13 | ])
14 | );
15 | // printf(''
16 | // , $delivery_auth_link
17 | // );
18 |
19 | $link = sprintf('/pos/delivery/ajax?%s'
20 | , http_build_query([
21 | 'a' => 'delivery-auth',
22 | 'c' => $_SESSION['Company']['id'],
23 | 'l' => $_SESSION['License']['id']
24 | ])
25 | );
26 | printf(''
27 | , $link
28 | );
29 |
30 | ?>
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
Active Couriers
39 |
78 |
79 |
80 |
81 |
Orders
82 |
83 |
84 |
85 |
86 |
87 | Order |
88 | Courier |
89 | Cart |
90 |
91 |
92 |
93 | $v) {
101 | if (preg_match('/^item\-(\w+)\-unit\-count$/', $k, $m)) {
102 | $b2c_item['item_list'][] = [
103 | 'id' => $m[1],
104 | ];
105 | }
106 | }
107 |
108 | echo '';
109 | printf('%s | ', $rec['id'], $rec['id']);
110 | printf('%s | ', $rec['contact_name']);
111 | printf('%d Items | ', count($b2c_item['item_list']));
112 | echo '
';
113 | }
114 | ?>
115 |
116 |
117 |
118 |
119 |
120 |
121 |
Map Loading...
122 |
123 |
124 |
125 |
126 |
127 |
128 |
182 |
--------------------------------------------------------------------------------
/view/pos/main.php:
--------------------------------------------------------------------------------
1 |
2 |
14 |
--------------------------------------------------------------------------------
/view/pos/online.php:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
Online Orders
11 |
These orders can come from external systems, if you have them configured or from your own website
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Order |
23 | Contact |
24 | Details |
25 |
26 |
27 |
28 | $v) {
35 | $item_info[] = sprintf('%s %s', $v['product']['name'], $v['variety']['name']);
36 | }
37 | $rec['item_info'] = implode(', ', $item_info);
38 |
39 | echo '';
40 | printf('%s | ', $rec['id']);
41 | printf('%s | ', $rec['contact_name']);
42 | // printf('%s | ', $rec['meta']);
43 | printf('%s | ', $rec['item_info']);
44 | echo '
';
45 |
46 | }
47 | ?>
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/view/pos/open.php:
--------------------------------------------------------------------------------
1 |
9 |
10 |
34 |
35 |
36 |
119 |
--------------------------------------------------------------------------------
/view/pos/terminal/shut.php:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
Terminal Session Closed
12 |
13 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/view/report/recent.php:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 | Sale |
15 | Date |
16 | Contact |
17 | Total |
18 | Status |
19 | |
20 |
21 |
22 |
23 |
24 | ';
33 | $action_ux.= '';
34 | $action_ux.= '';
35 | $action_ux = sprintf($action_ux, $b2c_transaction['id']);
36 | break;
37 | default:
38 | $status = 'Closed';
39 | $action_ux = '';
40 | }
41 | ?>
42 |
43 | = $b2c_transaction['guid'] ?> |
44 | = $b2c_transaction['created_at'] ?> |
45 | = $b2c_transaction['contact_id'] ?> |
46 | = $full_price ?> |
47 | = $status ?> |
48 | = $action_ux ?> |
49 |
50 |
53 |
54 |
55 |
56 |
57 |
58 |
76 |
77 |
--------------------------------------------------------------------------------
/view/shop/cart.php:
--------------------------------------------------------------------------------
1 | layout_file = sprintf('%s/view/_layout/shop-html.php', APP_ROOT);
7 |
8 | ?>
9 |
10 |
17 |
18 |
58 |
--------------------------------------------------------------------------------
/view/shop/checkout-done.php:
--------------------------------------------------------------------------------
1 | layout_file = sprintf('%s/view/_layout/shop-html.php', APP_ROOT);
7 |
8 | ?>
9 |
10 |
16 |
17 |
18 |
Order Complete! You will receive an email or phone confirmation shortly.
19 |
20 |
21 |
74 |
--------------------------------------------------------------------------------
/view/shop/checkout.php:
--------------------------------------------------------------------------------
1 | layout_file = sprintf('%s/view/_layout/shop-html.php', APP_ROOT);
7 |
8 | ?>
9 |
10 |
16 |
17 |
72 |
--------------------------------------------------------------------------------
/view/shop/example.php:
--------------------------------------------------------------------------------
1 | layout_file = sprintf('%s/view/_layout/shop-html.php', APP_ROOT);
7 |
8 | ?>
9 |
10 |
16 |
17 |
18 |
21 |
22 |
23 |
24 | = __h($p['product_name']) ?>
25 | = __h($p['variety_name']) ?>
26 |
27 |
28 |
39 |
40 | Inventory: = $p['id'] ?>
41 |
42 |
43 |
44 |
48 |
49 |
50 |
51 |
77 |
--------------------------------------------------------------------------------
/view/shop/main.php:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openthc/pos/d54b19901e8d4e075a7a8802eaca36e1bf9ef7f6/view/shop/main.php
--------------------------------------------------------------------------------
/webroot/.well-known/openthc/app:
--------------------------------------------------------------------------------
1 | app
2 |
--------------------------------------------------------------------------------
/webroot/.well-known/openthc/pos:
--------------------------------------------------------------------------------
1 | pos
2 |
--------------------------------------------------------------------------------
/webroot/css/shop.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Styles for the Public Shop and Example
3 | */
4 |
5 | .product-grid {
6 | display: flex;
7 | flex-direction: row;
8 | flex-wrap: wrap;
9 | justify-content: space-around;
10 | }
11 | .product-grid .product-item-wrap {
12 | flex: 1 1 25%;
13 | margin: 0 0 0.25rem 0;
14 | max-width: 30rem;
15 | min-width: 20rem;
16 | padding: 0.25rem;
17 | }
18 | .product-grid .product-item-wrap .product-item {
19 | border: 1px solid #ddd;
20 | border-radius: 0.25rem;
21 | }
22 | .product-grid .product-item-wrap .product-item header {
23 | margin: 0;
24 | padding: 0.25rem 0.50rem;
25 | }
26 | .product-grid .product-item-wrap .product-item header h2
27 | , .product-grid .product-item-wrap .product-item header h3 {
28 | margin: 0;
29 | padding: 0;
30 | }
31 |
32 | .product-grid .product-item-wrap .product-item .product-cost {
33 | font-size: 1.5rem;
34 | font-weight: bold;
35 | }
36 |
37 | .product-grid .product-item-wrap .product-item .product-item-foot {
38 | display: flex;
39 | margin: 0.50rem;
40 | flex-direction: row;
41 | justify-content: space-between;
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/webroot/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | POS || OpenTHC
9 |
29 |
30 |
31 |
32 |
33 |

34 |
35 |
36 | POS
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/webroot/js/pos-camera.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Camera Integration
3 | * @see https://www.damirscorner.com/blog/posts/20170901-DetectingCameraPermissionDialogInBrowser.html
4 | */
5 |
6 | (function(window) {
7 |
8 | 'use strict';
9 |
10 | var Camera = null;
11 | var Camera_auth = 0;
12 |
13 | window.OpenTHC = window.OpenTHC || {};
14 | window.OpenTHC.Camera = {
15 |
16 | /**
17 | *
18 | */
19 | exists: function(callback)
20 | {
21 | let dev_list = navigator.mediaDevices;
22 | if (!dev_list || !dev_list.enumerateDevices) return false;
23 | dev_list.enumerateDevices().then(devices => {
24 | callback(devices.some(device => 'videoinput' === device.kind));
25 | });
26 |
27 | },
28 |
29 | /**
30 | * Open the Camera
31 | */
32 | open: function(callback) {
33 |
34 | if (!Camera) {
35 | navigator.mediaDevices
36 | .getUserMedia({ audio: false, video: true })
37 | .then(function(stream) {
38 | Camera = stream;
39 | callback(stream);
40 | })
41 | .catch(function(err) { console.log(err); });
42 | }
43 |
44 | },
45 |
46 | scan: function(callback)
47 | {
48 | // Read from the camera until we get something
49 | // something == PDF417, QR or C128
50 |
51 | // Wait for QR Code
52 |
53 | // But Also show the Scanning Video On the Screen Somwehere
54 |
55 | }
56 |
57 | };
58 |
59 | })(window);
60 |
--------------------------------------------------------------------------------
/webroot/js/pos-modal-cart-options.js:
--------------------------------------------------------------------------------
1 | /**
2 | * [description]
3 | * @return {[type]} [description]
4 | */
5 |
6 |
7 | $(function() {
8 |
9 | $('#pos-cart-option-save').on('click', function() {
10 |
11 | OpenTHC.POS.Cart.date = $('#cart-option-date').val();
12 | OpenTHC.POS.Cart.time = $('#cart-option-time').val();
13 |
14 | // var $b = $(this);
15 | // $b.html(' Working...');
16 | // $b.attr('disabled', 'disabled');
17 |
18 | // var arg = {
19 | // a: 'checkout-option',
20 | // date: $('#checkout-option-date').val(),
21 | // time: $('#checkout-option-time').val(),
22 | // };
23 |
24 | // $.post('/pos/cart/ajax', arg, function(body, stat) {
25 | $('#pos-modal-cart-option').modal('hide');
26 | // });
27 |
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/webroot/js/pos-modal-discount.js:
--------------------------------------------------------------------------------
1 | /**
2 | * For the POS Modal for Discounts
3 | */
4 |
5 | $(function() {
6 |
7 | var adj = 0;
8 | var adj_note = 'Applied Discount';
9 | var base_price = 0;
10 | var full_price = 0;
11 | var item_taxes_total = 0;
12 |
13 | function _update_adj_new(adj)
14 | {
15 | $('.pos-checkout-sum-adj').html( (adj).toFixed(2) );
16 | $('.pos-checkout-sum-new').html( (base_price + adj).toFixed(2) );
17 | }
18 |
19 | // Do stuff on Modal Open
20 | $('#pos-modal-discount').on('show.bs.modal', function() {
21 |
22 | console.log('pos-modal-discount!show');
23 |
24 | base_price = parseFloat($('.pos-checkout-sum').first().html()) || 0;
25 |
26 | // $('#pos-modal-discount-list').load('/pos/ajax', { a: 'discount-list' });
27 |
28 | });
29 |
30 | // Detect Change and Reset Other One (fix resets pct)
31 | $('#pos-checkout-discount-fix').on('blur keyup', function(e) {
32 |
33 | var val = this.value;
34 | var fix = Math.abs(parseFloat(val) || 0);
35 | $('#pos-checkout-discount-pct').val('');
36 |
37 | adj = fix * -1;
38 | adj_note = `Applied Discount $${adj.toFixed(2)}`;
39 | _update_adj_new(adj);
40 |
41 | });
42 |
43 | // Percent Discount
44 | $('#pos-checkout-discount-pct').on('blur keyup', function(e) {
45 |
46 | var val = this.value;
47 | var pct = Math.abs(parseFloat(val) || 0);
48 | $('#pos-checkout-discount-fix').val('');
49 |
50 | if (pct <= 1) {
51 | pct = pct * 100;
52 | }
53 | adj = (base_price * pct / 100) * -1;
54 | adj_note = `Applied Discount ${pct.toFixed(0)}%`;
55 |
56 | _update_adj_new(adj);
57 |
58 | });
59 |
60 | // Add Line Item
61 | $('#pos-discount-apply').on('click', function() {
62 | Cart_addItem({
63 | id: window.ulid(),
64 | qty: 1,
65 | name: adj_note,
66 | price: adj, // v1
67 | unit_price: adj // v2
68 | });
69 | });
70 |
71 | });
72 |
--------------------------------------------------------------------------------
/webroot/js/pos-modal-loyalty.js:
--------------------------------------------------------------------------------
1 | /**
2 | * [description]
3 | * @return {[type]} [description]
4 | */
5 |
6 |
7 | $(function() {
8 |
9 | $('#pos-loyalty-apply').on('click', function() {
10 |
11 | var $b = $(this);
12 | $b.html(' Working...');
13 | $b.attr('disabled', 'disabled');
14 |
15 | var arg = {
16 | a: 'loyalty',
17 | phone: $('#loyalty-phone').val(),
18 | email: $('#loyalty-email').val(),
19 | other: $('#loyalty-other').val(),
20 | };
21 |
22 | $.post('/pos/cart/ajax', arg, function(body, stat) {
23 | Cart_addItem({
24 | id: 0,
25 | name: body.data.name,
26 | weight: '-',
27 | price: body.data.rank,
28 | size: 1,
29 | });
30 | $('#pos-modal-loyalty').modal('hide');
31 | })
32 |
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/webroot/js/pos-modal-payment.js:
--------------------------------------------------------------------------------
1 | /**
2 | * JavaScript Handlers for the POS Payment Modal
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-only
5 | */
6 |
7 | var ppCashPaid_List = new Array();
8 |
9 | function ppFormUpdate()
10 | {
11 | var full = parseFloat(OpenTHC.POS.Cart.full_price);
12 | var cash = parseFloat($('#payment-cash-incoming').text());
13 | var need = (full - cash);
14 | var back = (cash - full);
15 |
16 | console.log('ppFormUpdate(' + cash + ', ' + full + ')');
17 |
18 |
19 | if (cash < full) {
20 |
21 | $('#amount-paid-wrap').removeClass('alert-success');
22 | $('#amount-paid-wrap').addClass('alert-secondary');
23 |
24 | $('#amount-need-wrap').removeClass('alert-danger alert-secondary alert-success');
25 | $('#amount-need-wrap').addClass('alert-warning');
26 | $('#amount-need-hint').text('Due:');
27 | $('#payment-cash-outgoing').text(need.toFixed(2));
28 |
29 | } else if (cash === full) {
30 |
31 | $('#amount-paid-wrap').removeClass('alert-secondary alert-success');
32 | $('#amount-paid-wrap').addClass('alert-success');
33 |
34 | $('#amount-need-wrap').removeClass('alert-danger alert-secondary alert-success alert-warning');
35 | $('#amount-need-wrap').addClass('alert-success');
36 | $('#amount-need-hint').text('Perfect!');
37 | $('#payment-cash-outgoing').text('0.00');
38 |
39 | // $('#payment-cash-incoming').addClass('text-success').removeClass('text-danger').removeClass('text-warning');
40 |
41 | $('#pos-payment-commit').removeAttr('disabled');
42 |
43 | } else if (cash > full) {
44 |
45 | $('#amount-paid-wrap').removeClass('alert-secondary alert-success');
46 | $('#amount-paid-wrap').addClass('alert-success');
47 |
48 | // Making Change
49 | $('#amount-need-wrap').removeClass('alert-danger alert-secondary alert-success');
50 | $('#amount-need-wrap').addClass('alert-danger');
51 | $('#amount-need-hint').text('Change:');
52 | $('#payment-cash-outgoing').text( back.toFixed(2) );
53 |
54 | // $('#payment-cash-incoming').addClass('text-warning').removeClass('text-danger').removeClass('text-success');
55 | // $('#pp-card-pay').removeClass('text-danger').removeClass('text-success').removeClass('text-warning');
56 |
57 | $('#pos-payment-commit').removeAttr('disabled');
58 |
59 | }
60 |
61 | if (ppCashPaid_List.length == 0) {
62 | $('#pos-pay-undo').prop('disabled', true);
63 | } else {
64 | $('#pos-pay-undo').prop('disabled', false);
65 | }
66 |
67 | }
68 |
69 |
70 | function ppAddCash(n)
71 | {
72 | console.log('ppAddCash');
73 |
74 | var full = OpenTHC.POS.Cart.full_price;
75 |
76 | var add = parseFloat( $(n).data('amount') );
77 | var cur = parseFloat( $('#payment-cash-incoming').text() );
78 | if (!cur) cur = 0;
79 |
80 | var cash = (cur + add);
81 | $('#payment-cash-incoming').text( cash.toFixed(2) );
82 |
83 | var card = full - cash;
84 | $('#pp-card-pay').val( card.toFixed(2) );
85 |
86 | ppCashPaid_List.push(add);
87 |
88 | ppFormUpdate();
89 |
90 | }
91 |
92 | function ppAddCard()
93 | {
94 | console.log('ppAddCard');
95 |
96 | var need = $('#payment-need').val();
97 | $('#pp-card-confirm').val(need);
98 |
99 | //Weed.modal('shut');
100 |
101 | // var arg = $('#psi-form').serializeArray();
102 | // $('#modal-content-wrap').load('/pos/pay', arg, function() {
103 | // $.post('/pos/pay', arg, function(res) {
104 |
105 | // var x = $('#card-modal').clone();
106 | // $(x).find('#pp-card-confirm').attr('id', 'pp-card-prompt');
107 | //Weed.modal( $('#card-modal') );
108 | //$('#card-modal').show();
109 |
110 |
111 | // Weed.modal('#card-modal');
112 | // $('#pp-card-confirm').val( $('#pp-card-pay').val() );
113 | // Weed.modal('#card-modal');
114 | // });
115 | }
116 |
117 | $(function() {
118 |
119 | $('.pp-cash').on('click touchend', function(e) {
120 | ppAddCash(this);
121 | e.preventDefault();
122 | e.stopPropagation();
123 | return false;
124 | });
125 |
126 | $('.pp-card').on('click touchend', function(e) {
127 | ppAddCard();
128 | e.preventDefault();
129 | e.stopPropagation();
130 | return false;
131 | });
132 |
133 | // Focus on Select!
134 | // $('#payment-cash-incoming').on('focus', function(e) {
135 | // console.log('focus');
136 | // $(this).select();
137 | // });
138 | // $('#payment-cash-incoming').on('mouseup', function(e) {
139 | // console.log('mouseup');
140 | // e.preventDefault();
141 | // return false;
142 | // });
143 |
144 | // $('#payment-cash-incoming').on('keyup', function() {
145 | // ppFormUpdate();
146 | // });
147 |
148 | // Reset my Form
149 | $('#pos-pay-undo').on('click', function() {
150 | $('#payment-cash-incoming').text('0.00');
151 | $('#pp-card-pay').text('0.00');
152 | ppFormUpdate();
153 | });
154 |
155 | $(document.body).on('click', '#pos-card-back', function() {
156 | //Weed.modal('shut');
157 | });
158 |
159 | $(document.body).on('click', '#pos-card-done', function() {
160 | //Weed.modal('shut');
161 | });
162 |
163 | /**
164 | Actual Payment Button
165 | */
166 | $('#pos-payment-commit').on('click touchend', function(e) {
167 |
168 | var cash_incoming = $('#payment-cash-incoming').text();
169 | var cash_outgoing = $('#payment-cash-outgoing').text();
170 |
171 | // Append to existing form to capture all the other existing inputs
172 | $('#psi-form').attr('action', '/pos/checkout/commit');
173 | $('#psi-form').append('');
174 | $('#psi-form').append(``);
175 | $('#psi-form').append(``);
176 | $('#psi-form').append(``);
177 | $('#psi-form').append('');
178 | $('#psi-form').append(``);
179 | $('#psi-form').append(``);
180 | $('#psi-form').submit();
181 |
182 | return false;
183 |
184 | });
185 |
186 | });
187 |
--------------------------------------------------------------------------------
/webroot/js/pos-printer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Print Helper
3 | *
4 | * SPDX-License-Identifier: GPL-3.0-only
5 | */
6 |
7 | POS.Printer = {
8 |
9 | /**
10 | *
11 | */
12 | printLocalBrowser: function()
13 | {
14 |
15 | },
16 |
17 | /**
18 | * Download the Document as PDF and then send to our HTTP Print Queue
19 | */
20 | printLocalNetwork: function(pdf_url, printer_url)
21 | {
22 | console.log('POS.Printer.printLocalNetwork()');
23 |
24 | // Big AJAX
25 | $.ajax({
26 | type: 'POST',
27 | url: pdf_url,
28 | data: {
29 | a: 'send-print',
30 | },
31 | dataType: 'binary',
32 | xhrFields: {
33 | responseType: 'blob',
34 | },
35 | success: function(body, ret) {
36 |
37 | // Then another Big AJAX
38 | var FD = new FormData();
39 | FD.append('a', 'print-file');
40 | FD.append('file', body);
41 |
42 | $.ajax({
43 | type: "POST",
44 | url: printer_url,
45 | cache: false,
46 | contentType: false,
47 | processData: false,
48 | data: FD,
49 | xhrFields:{
50 | responseType: 'blob'
51 | },
52 | });
53 | }
54 | });
55 |
56 | },
57 |
58 | /**
59 | *
60 | */
61 | printServerConnect: function() {
62 |
63 |
64 | },
65 |
66 | };
67 |
--------------------------------------------------------------------------------
/webroot/js/pos-scanner.js:
--------------------------------------------------------------------------------
1 | /**
2 | * POS Scanner
3 | * Tries to be a Helper for reading PDF417 from Bluetooth or USB Scanner
4 | */
5 |
6 |
7 | var POS = POS || {};
8 |
9 | POS.Scanner = {};
10 | POS.Scanner.callback = undefined;
11 | POS.Scanner.data = [];
12 |
13 | POS.Scanner.done = function()
14 | {
15 | var data = POS.Scanner.data.join(' ');
16 | data = data.replace(/Shift/g, ' ');
17 | data = data.replace(/Control J/g, '
--Control J--
');
18 | data = data.replace(/\[CR\]$/g, '');
19 | $('#scan-input-data').html(data);
20 |
21 | if (POS.Scanner.callback) {
22 | POS.Scanner.callback(POS.Scanner.data);
23 | POS.Scanner.callback = undefined;
24 | }
25 | }
26 |
27 |
28 | POS.Scanner.live = function(node, callback)
29 | {
30 | POS.Scanner.callback = callback;
31 | POS.Scanner.data = [];
32 | $(document.body).on('keydown', POS.Scanner.read);
33 | $(node).html('Ready to Scan');
34 |
35 | }
36 |
37 | /**
38 | * Read A Character
39 | * @param {[type]} e [description]
40 | * @return {[type]} [description]
41 | */
42 | POS.Scanner.read = function(e)
43 | {
44 | //console.log(e);
45 |
46 | var c = String.fromCharCode(e.which);
47 |
48 | console.log({
49 | c: c,
50 | which: e.which,
51 | key: e.key,
52 | keyCode: e.keyCode,
53 | char: e.char,
54 | charCode: e.charCode,
55 | alt: e.altKey,
56 | ctrl: e.ctrlKey,
57 | meta: e.metaKey,
58 | shift: e.shiftKey,
59 | });
60 |
61 | switch (e.which) {
62 | case 13: // Enter
63 | POS.Scanner.stop();
64 | POS.Scanner.done();
65 | break;
66 | case 16: // Shift
67 | case 17: // Ctrl
68 | case 18: // Alt
69 | c = e.key;
70 | break;
71 | case 91: // Windows Key
72 | case 93: // Windows Context Key
73 | return false;
74 | case 186:
75 | if (e.shiftKey) {
76 | c = ':';
77 | } else {
78 | c = ';';
79 | }
80 | break;
81 | case 190:
82 | c = '.';
83 | if (e.shiftKey) {
84 | c = '>';
85 | }
86 | break;
87 | case 191: // /
88 | c = '/';
89 | if (e.shiftKey) {
90 | c = '?';
91 | }
92 | break;
93 | case 192:
94 | if (e.shiftKey) {
95 | c = '~';
96 | } else {
97 | c = '`';
98 | }
99 | break;
100 | }
101 |
102 | //POS.Scanner.data.push(e.keyCode);
103 | POS.Scanner.data.push(c);
104 | return false;
105 |
106 | }
107 |
108 | POS.Scanner.stop = function()
109 | {
110 | $(document.body).off('keydown', POS.Scanner.read);
111 | }
112 |
--------------------------------------------------------------------------------
/webroot/loading.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Loading...
7 |
39 |
40 |
41 |
42 |
45 |
46 |
47 |
48 |
49 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/webroot/main.php:
--------------------------------------------------------------------------------
1 | .
18 | *
19 | * SPDX-License-Identifier: GPL-3.0-only
20 | */
21 |
22 | require_once(dirname(dirname(__FILE__)) . '/boot.php');
23 |
24 | // Slim Application
25 | $cfg = [];
26 | $cfg['debug'] = true;
27 | $app = new \OpenTHC\App($cfg);
28 |
29 |
30 | // Container
31 | $con = $app->getContainer();
32 | $con['DB'] = function($c) {
33 |
34 | $dbc = null;
35 |
36 | if (!empty($_SESSION['dsn'])) {
37 | $dbc = _dbc($_SESSION['dsn']);
38 | }
39 |
40 | return $dbc;
41 |
42 | };
43 |
44 |
45 | // Redis Connection
46 | $con['Redis'] = function($c) {
47 | $x = \OpenTHC\Config::get('redis/hostname');
48 | $r = new \Redis();
49 | $r->connect($x);
50 | return $r;
51 | };
52 |
53 |
54 | // Get Current Company Object
55 | $con['Company'] = function($c0) {
56 |
57 | static $C;
58 |
59 | if (empty($C)) {
60 |
61 | $dbc = $c0->DB;
62 | $C = new \OpenTHC\Company($dbc, $_SESSION['Company']);
63 |
64 | }
65 |
66 | return $C;
67 |
68 | };
69 |
70 |
71 | // API
72 | $app->group('/api/v2018', 'OpenTHC\POS\Module\API');
73 |
74 |
75 | // Main Page
76 | $app->group('/dashboard', 'OpenTHC\POS\Module\Dashboard')
77 | ->add('OpenTHC\POS\Middleware\Auth')
78 | ->add('OpenTHC\Middleware\Session');
79 |
80 |
81 | // POS / Register
82 | $app->group('/pos', 'OpenTHC\POS\Module\POS')
83 | ->add('OpenTHC\POS\Middleware\Auth')
84 | ->add('OpenTHC\Middleware\Session');
85 |
86 |
87 | // CRM / Loyalty
88 | $app->group('/crm', 'OpenTHC\POS\Module\CRM')
89 | ->add('OpenTHC\POS\Middleware\Auth')
90 | ->add('OpenTHC\Middleware\Session');
91 |
92 |
93 | // B2B Operations
94 | $app->group('/report', 'OpenTHC\POS\Module\Report')
95 | ->add('OpenTHC\POS\Middleware\Auth')
96 | ->add('OpenTHC\Middleware\Session');
97 |
98 |
99 | // Onsite & Online menus
100 | $app->group('/menu', 'OpenTHC\POS\Module\Menu')
101 | ->add('OpenTHC\POS\Middleware\Auth')
102 | ->add('OpenTHC\Middleware\Session');
103 |
104 |
105 | // External Shop
106 | $app->group('/shop', 'OpenTHC\POS\Module\Shop')
107 | ->add('OpenTHC\Middleware\Session');
108 |
109 |
110 | // CRM / Loyalty
111 | $app->get('/contact/ajax', 'OpenTHC\POS\Controller\Contact')
112 | ->add('OpenTHC\POS\Middleware\Auth')
113 | ->add('OpenTHC\Middleware\Session');
114 |
115 | // Vendor
116 | // $app->group('/vendor', 'OpenTHC\POS\Module\Vendor')
117 | // ->add('OpenTHC\POS\Middleware\Auth')
118 | // ->add('OpenTHC\Middleware\Session');
119 |
120 |
121 | // Authentication
122 | $app->group('/auth', function() {
123 | $this->get('', 'OpenTHC\POS\Controller\Auth\oAuth2\Open');
124 | $this->get('/open', 'OpenTHC\POS\Controller\Auth\oAuth2\Open');
125 | $this->get('/back', 'OpenTHC\POS\Controller\Auth\oAuth2\Back');
126 | $this->get('/init', 'OpenTHC\POS\Controller\Auth\Init')->setName('auth/init');
127 | $this->get('/ping', 'OpenTHC\Controller\Auth\Ping');
128 | $this->get('/shut', 'OpenTHC\POS\Controller\Auth\Shut');
129 | })
130 | ->add('OpenTHC\Middleware\Session');
131 |
132 |
133 | // Intent
134 | $app->map(['GET','POST'], '/intent', 'OpenTHC\POS\Controller\Intent')
135 | ->add('OpenTHC\Middleware\Session');
136 |
137 |
138 | // Webhooks
139 | $app->group('/webhook', 'OpenTHC\POS\Module\Webhook');
140 |
141 |
142 | // Execute
143 | $app->run();
144 |
145 | exit(0);
146 |
--------------------------------------------------------------------------------
/webroot/robots.txt:
--------------------------------------------------------------------------------
1 | user-agent: *
2 | disallow: /
3 |
--------------------------------------------------------------------------------