diff --git a/.gitignore b/.gitignore index e6632d3..76849f4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,11 @@ # Build /bin/ /release/ +/dist/ /vendor/ +__pycache__/ +/apizza +release-notes* # Notes demo @@ -9,22 +13,27 @@ TODO *.txt # Tests +/test/ +coverage.html *.json *.test +!Dockerfile.test *.py -test -example -test-coverage -test-cover -coverage.html *.prof -/data/ !dawg/testdata/cardnums.json +/env/ +dawg/ratelimit_test.go + +# Tools +/.vscode +*.code-workspace +scripts/history.sh +scripts/github.sh +scripts/test-versions.sh +scripts/release +scripts/release.sh # Other -.vscode *.out *.exe -/script/ -scripts/history.sh -*.mp4 \ No newline at end of file +*.mp4 diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..406c34d --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,95 @@ +project_name: apizza + +before: + hooks: + - go mod tidy + - go generate ./... + +release: + github: + owner: harrybrwn + name: apizza + prerelease: auto + draft: false + +builds: + - binary: apizza + id: apizza + env: + - CGO_ENABLED=0 + - GO111MODULE=on + ldflags: + - -s -w + - -X github.com/harrybrwn/apizza/cmd.version={{.Version}} + goos: [linux, darwin, windows] + goarch: [386, amd64, arm64] + ignore: + - { goos: darwin, goarch: 386 } + - { goos: darwin, goarch: arm64 } + - { goos: windows, goarch: arm64 } + +archives: + - replacements: &replacements + darwin: MacOS + linux: Linux + windows: Windows + 386: 32-bit + amd64: 64-bit + arm64: arm64 + format_overrides: + - goos: windows + format: zip + files: + - LICENSE + - README.md + - docs/* + +nfpms: + - <<: &description + description: Command line tool for ordering Dominos pizza. + file_name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + replacements: *replacements + maintainer: Harry Brown + license: Apache 2.0 + formats: + - deb + - rpm + bindir: /usr/local/bin + +brews: + - <<: *description + name: apizza + github: + owner: harrybrwn + name: homebrew-tap + homepage: https://github.com/harrybrwn/apizza + commit_author: + name: apizza-releasebot + email: harrybrown98@gmail.com + folder: Formula + test: | + system "#{bin}/apizza --version" + install: | + bin.install "apizza" + +snapcrafts: + - <<: *description + name_template: '{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' + summary: Command line tool for ordering Dominos pizza. + grade: stable + confinement: classic + publish: false + +checksum: + name_template: 'checksums.txt' + +snapshot: + name_template: "{{ .Version }}-{{ .ShortCommit }}" + +changelog: + skip: true + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/.travis.yml b/.travis.yml index a2d5cfa..bb3f6d2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,8 @@ language: go go: - - 1.11 - - 1.12 - 1.13 + - 1.14.1 env: global: @@ -15,8 +14,11 @@ os: - osx - windows +before_install: source scripts/before_install.sh + before_script: - - bash scripts/build.sh + - bash scripts/build.sh test + - go get -u github.com/rakyll/gotest script: - bash scripts/test.sh @@ -26,4 +28,4 @@ after_success: - bash <(curl -s https://codecov.io/bash) notifications: - email: false \ No newline at end of file + email: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d92e833 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,18 @@ +# Contributing to apizza + +So I have very little experience contributing or running open source projects so just like make a pull request or whatever. + +I'll keep it pretty chill, but just make sure you follow theses guidlines: +- Test your code before you commit it +- Run `gofmt` to format the code +- Work off of the dev branch + +#### Issuse +If you find something wrong just file a new issue and I'll do my best to address it. + +#### Testing +The tests for the dawg package use two environment variables for the dominos testing account. + +`DOMINOS_TEST_USER` is the email for the dominos testing account + +`DOMINOS_TEST_PASS` is the password for the testing account diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dbcd749 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.14.2-buster + +RUN apt-get install make +RUN go get golang.org/x/tools/cmd/stringer + +ADD . /go/src/github.com/harrybrwn/apizza +WORKDIR /go/src/github.com/harrybrwn/apizza + +RUN make install + +ENTRYPOINT ["apizza"] diff --git a/Dockerfile.test b/Dockerfile.test new file mode 100644 index 0000000..8304fcc --- /dev/null +++ b/Dockerfile.test @@ -0,0 +1,13 @@ +FROM golang:1.14.2-buster + +RUN apt-get install make +RUN go get golang.org/x/tools/cmd/stringer +RUN go get github.com/rakyll/gotest + +ADD . /go/src/github.com/harrybrwn/apizza +WORKDIR /go/src/github.com/harrybrwn/apizza + +RUN make install + +RUN which make +CMD ["make", "test"] diff --git a/LICENSE b/LICENSE index 1db069a..2bd1a2b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,202 +1,202 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright © 2019 Harrison Brown harrybrown98@gmail.com - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright © 2019 Harrison Brown harrybrown98@gmail.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile index 80397d2..ebbbae2 100644 --- a/Makefile +++ b/Makefile @@ -1,35 +1,51 @@ COVER=go tool cover -all: install +#VERSION=$(shell git describe --tags --abbrev=12) +VERSION=$(shell git describe --tags --abbrev=0)-$(shell git rev-parse --short HEAD) +GOFLAGS=-ldflags "-X $(shell go list)/cmd.version=$(VERSION)" -install: - go install github.com/harrybrwn/apizza +build: gen + go build $(GOFLAGS) -uninstall: - $(RM) "$$GOPATH/bin/apizza" +install: gen + go install $(GOFLAGS) -build: - go build -o bin/apizza +uninstall: clean + go clean -i -release: - bash scripts/release.sh +test: test-build + bash scripts/test.sh + bash scripts/integration.sh ./bin/test-apizza + @[ -d ./bin ] && [ -x ./bin/test-apizza ] && rm -rf ./bin + +docker: + docker build --rm -t apizza . + +docker-test: + docker build -f Dockerfile.test --rm -t apizza:$(VERSION) . + docker run --rm -it apizza:$(VERSION) -test: coverage.txt test-build - bash scripts/integration.sh ./bin/apizza - @[ -d bin ] && rm -rf bin +release: gen + scripts/release build -test-build: - go build -o bin/apizza -ldflags "-X cmd.enableLog=false" +test-build: gen + scripts/build.sh test coverage.txt: - bash scripts/test.sh + @ echo '' > coverage.txt + go test -v ./... -coverprofile=coverage.txt -covermode=atomic html: coverage.txt $(COVER) -html=$< +gen: + go generate ./... + clean: - $(RM) coverage.txt - $(RM) -r release bin + $(RM) -r coverage.txt release/apizza-* bin dist go clean -testcache + go clean + +all: test build release -.PHONY: install test clean html release \ No newline at end of file +.PHONY: install test clean html release gen docker diff --git a/README.md b/README.md index bb454d1..a5b618e 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,148 @@ -![apizza logo](/docs/logo.png) - -[![Build Status](https://travis-ci.com/harrybrwn/apizza.svg?branch=master)](https://travis-ci.com/harrybrwn/apizza) -[![GoDoc](https://godoc.org/github.com/github.com/harrybrwn/apizza/dawg?status.svg)](https://godoc.org/github.com/harrybrwn/apizza/dawg) -[![Go Report Card](https://goreportcard.com/badge/github.com/harrybrwn/apizza)](https://goreportcard.com/report/github.com/harrybrwn/apizza) -[![codecov](https://codecov.io/gh/harrybrwn/apizza/branch/master/graph/badge.svg)](https://codecov.io/gh/harrybrwn/apizza) - -Dominos pizza from the command line. - -### Table of Contents -- [Installation](#installation) -- [Setup](#setup) -- [Commands](#commands) - - [Config](#config) - - [Cart](#cart) - - [Menu](#menu) -- [DAWG](#the-dominos-api-wrapper-for-go) - -### Installation -```bash -go get -u github.com/harrybrwn/apizza -go install github.com/harrybrwn/apizza -``` - -### Setup -The most you have to do as a user in terms of setting up apizza is fill in the config variables. The only config variables that are manditory are "Address" and "Service" but the other config variables contain information that the Dominos website uses. - -> **Note**: The config file won't exist if apizza is not run at least once. - -To edit the config file, you can either use the built-in `config get` and `config set` commands (see [Config](#config)) to configure apizza or you can edit the `$HOME/.apizza/config.json` file. Both of these setup methods will have the same results If you add a key-value pair to the `config.json` file that is not already in the file it will be overwritten the next time the program is run. - - -### Config -The `config get` and `config set` commands can be used with one config variable at a time... -```bash -apizza config set email='bob@example.com' -apizza config set name='Bob' -apizza config set service='Carryout' -``` -or they can be moved to one command like so. -```bash -apizza config set name=Bob email='bob@example.com' service='Carryout' -``` - -Or just edit the json config file with -```bash -apizza config --edit -``` - - -### Cart -To save a new order, use `apizza cart new` -```bash -apizza cart new 'testorder' --products=16SCREEN,2LCOKE -``` -`apizza cart` is the command the shows all the saved orders. - -The two flags `--add` and `--remove` are intended for editing an order. They will not work if no order name is given as a command. To add a product from an order, simply give `apizza cart --add=` and to remove a product give `--remove=`. - -Editing a product's toppings a little more complicated. The `--product` flag is the key to editing toppings. To edit a topping, give the product that the topping belogns to to the `--product` flag and give the actual topping name to either `--remove` or `--add`. - -```bash -apizza cart myorder --product=16SCREEN --add=P -``` -This command will add pepperoni to the pizza named 16SCREEN, and... -```bash -apizza cart myorder --product=16SCREEN --remove=P -``` -will remove pepperoni from the 16SCREEN item in the order named 'myorder'. - - -### Menu -Run `apizza menu` to print the dominos menu. - -The menu command will also give more detailed information when given arguments. - -The arugments can either be a product code or a category name. -```bash -apizza menu pizza # show all the pizza -apizza menu drinks # show all the drinks -apizza menu 10SCEXTRAV # show details on 10SCEXTRAV -``` -To see the different menu categories, use the `--show-categories` flag. And to view the different toppings use the `--toppings` flag. - -### The [Domios API Wrapper for Go](/docs/dawg.md) - -> **Credit**: Logo was made with [Logomakr](https://logomakr.com/). \ No newline at end of file +![apizza logo](/docs/logo.png) + +[![Build Status](https://travis-ci.com/harrybrwn/apizza.svg?branch=master)](https://travis-ci.com/harrybrwn/apizza) +[![GoDoc](https://godoc.org/github.com/github.com/harrybrwn/apizza/dawg?status.svg)](https://pkg.go.dev/github.com/harrybrwn/apizza/dawg?tab=doc) +[![Go Report Card](https://goreportcard.com/badge/github.com/harrybrwn/apizza)](https://goreportcard.com/report/github.com/harrybrwn/apizza) +[![codecov](https://codecov.io/gh/harrybrwn/apizza/branch/master/graph/badge.svg)](https://codecov.io/gh/harrybrwn/apizza) +[![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/harrybrwn/apizza)](https://www.tickgit.com/browse?repo=github.com/harrybrwn/apizza) + +Dominos pizza from the command line. + +## Table of Contents +- [Installation](#installation) +- [Setup](#setup) +- [Commands](#commands) + - [Config](#config) + - [Menu](#menu) + - [Cart](#cart) + - [Order](#order) +- [Tutorials](#tutorials) + - [None Pizza with Left Beef](#none-pizza-with-left-beef) + +## Installation +Download the precompiled binaries for Mac, Windows, and Linux + +##### Homebrew +``` +brew install harrybrwn/tap/apizza +``` +##### Debian/Ubuntu +``` +curl -LO https://github.com/harrybrwn/apizza/releases/download/v0.0.3/apizza_0.0.3_Linux_64-bit.deb +sudo dpkg -i apizza_0.0.3_Linux_64-bit.deb +``` +##### Rpm +``` +curl -LO https://github.com/harrybrwn/apizza/releases/download/v0.0.3/apizza_0.0.3_Linux_64-bit.rpm +sudo rpm -i apizza_0.0.3_Linux_64-bit.rpm +``` +##### Archives +- MacOS
+- Linux +- Windows + - 64 bit + - 32 bit + +#### Compile +```bash +go get -u github.com/harrybrwn/apizza +``` +or +```bash +git clone https://github.com/harrybrwn/apizza +cd apizza +make install +``` + +## Setup +The most you have to do as a user in terms of setting up apizza is fill in the config variables. The only config variables that are mandatory are "Address" and "Service" but the other config variables contain information that the Dominos website uses. + +To edit the config file, you can either use the built-in `config get` and `config set` commands (see [Config](#config)) to configure apizza or you can edit the `$HOME/.config/apizza/config.json` file. Both of these setup methods will have the same results If you add a key-value pair to the `config.json` file that is not already in the file it will be overwritten the next time the program is run. + + +## Config +For documentation on configuration and configuration fields, see [documentation](/docs/configuration.md) + +The `config get` and `config set` commands can be used with one config variable at a time... +```sh +$ apizza config set email='bob@example.com' +$ apizza config set name='Bob' +$ apizza config set service='Carryout' +``` + +Or they can be moved to one command like so. +```bash +$ apizza config set name=Bob email='bob@example.com' service='Carryout' +``` + +Or just edit the json config file with +```bash +$ apizza config --edit +``` + + +## Menu +Run `apizza menu` to print the dominos menu. + +The menu command will also give more detailed information when given arguments. + +The arguments can either be a product code or a category name. +```bash +$ apizza menu pizza # show all the pizza +$ apizza menu drinks # show all the drinks +$ apizza menu 10SCEXTRAV # show details on 10SCEXTRAV +``` +To see the different menu categories, use the `--show-categories` flag. And to view the different toppings use the `--toppings` flag. + + +## Cart +To save a new order, use `apizza cart new` +```bash +$ apizza cart new 'testorder' --product=16SCREEN --toppings=P,C,X # pepperoni, cheese, sauce +``` +`apizza cart` is the command the shows all the saved orders. + +> Note: Adding and removing items from the cart is a little bit weird and it will probably change in the future. + +The two flags `--add` and `--remove` are intended for editing an order. They will not work if no order name is given as a command. To add a product from an order, simply give `apizza cart --add=` and to remove a product give `--remove=`. + +Editing a product's toppings a little more complicated. The `--product` flag is the key to editing toppings. To edit a topping, give the product that the topping belongs to to the `--product` flag and give the actual topping name to either `--remove` or `--add`. + +```bash +$ apizza cart myorder --product=16SCREEN --add=P +``` +This command will add pepperoni to the pizza named 16SCREEN, and... +```bash +$ apizza cart myorder --product=16SCREEN --remove=P +``` +will remove pepperoni from the 16SCREEN item in the order named 'myorder'. + +To customize toppings use the syntax `::<0.5|1.0|1.5|2.0>` when adding a topping. +```sh +$ apizza cart myorder --product=12SCREEN --add=P:full:2 # double pepperoni +``` + + +## Order +To actually send an order from the cart. Use the `order` command. + +```bash +$ apizza order myorder --cvv=000 +``` +Once the command is executed, it will prompt you asking if you are sure you want to send the order. Enter `y` and the order will be sent. + +## Tutorials + +#### None Pizza with Left Beef +```bash +$ apizza cart new --name=leftbeef --product=12SCREEN +$ apizza cart leftbeef --remove=C --product=12SCREEN # remove cheese +$ apizza cart leftbeef --remove=X --product=12SCREEN # remove sauce +$ apizza cart leftbeef --add=B:left --product=12SCREEN # add beef to the left +``` + + +### The [Dominos API Wrapper for Go](/docs/dawg.md) +Docs and example code for my Dominos library. + +> **Credit**: Logo was made with [Logomakr](https://logomakr.com/). diff --git a/cmd/apizza.go b/cmd/apizza.go index fd8f768..4fc25de 100644 --- a/cmd/apizza.go +++ b/cmd/apizza.go @@ -15,14 +15,14 @@ package cmd import ( + "errors" "fmt" "log" "os" fp "path/filepath" - "strings" "github.com/harrybrwn/apizza/cmd/cli" - "github.com/harrybrwn/apizza/cmd/command" + "github.com/harrybrwn/apizza/cmd/commands" "github.com/harrybrwn/apizza/pkg/config" "github.com/spf13/cobra" "gopkg.in/natefinch/lumberjack.v2" @@ -37,31 +37,24 @@ var Logger = &lumberjack.Logger{ Compress: false, } -const enableLog = true +var ( + // Version is the cli version id (will be set as an ldflag) + version string + + // testing version change this with an ldflag + enableLog = "yes" +) // AllCommands returns a list of all the Commands. func AllCommands(builder cli.Builder) []*cobra.Command { return []*cobra.Command{ - NewCartCmd(builder).Cmd(), - command.NewConfigCmd(builder).Cmd(), + commands.NewCartCmd(builder).Cmd(), + commands.NewConfigCmd(builder).Cmd(), NewMenuCmd(builder).Cmd(), - NewOrderCmd(builder).Cmd(), - } -} - -// ErrMsg is not actually an error but it is my way of -// containing an error with a message and an exit code. -type ErrMsg struct { - Msg string - Code int - Err error -} - -func senderr(e error, msg string, code int) *ErrMsg { - if e == nil { - return nil + commands.NewOrderCmd(builder).Cmd(), + commands.NewAddAddressCmd(builder, os.Stdin).Cmd(), + commands.NewCompletionCmd(builder), } - return &ErrMsg{Msg: msg, Code: code, Err: e} } // Execute runs the root command @@ -72,7 +65,7 @@ func Execute(args []string, dir string) (msg *ErrMsg) { return senderr(err, "Internal Error", 1) } - if enableLog { + if enableLog == "yes" { Logger.Filename = fp.Join(config.Folder(), "logs", "dev.log") log.SetOutput(Logger) } @@ -87,24 +80,46 @@ func Execute(args []string, dir string) (msg *ErrMsg) { }() cmd := app.Cmd() + cmd.Version = version cmd.SetArgs(args) cmd.AddCommand(AllCommands(app)...) return senderr(cmd.Execute(), "Error", 1) } -var test = false +// ErrMsg is not actually an error but it is my way of +// containing an error with a message and an exit code. +type ErrMsg struct { + Msg string + Code int + Err error +} -func yesOrNo(in *os.File, msg string) bool { - var res string - fmt.Printf("%s ", msg) - _, err := fmt.Fscan(in, &res) - if err != nil { - return false +func senderr(e error, msg string, code int) *ErrMsg { + if e == nil { + return nil } + return &ErrMsg{Msg: msg, Code: code, Err: e} +} + +var test = false + +func newTestCmd(b cli.Builder, valid bool) *cobra.Command { + return &cobra.Command{ + Use: "test", + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + if !valid { + return errors.New("no such command 'test'") + } + + db := b.DB() + fmt.Printf("%+v\n", db) - switch strings.ToLower(res) { - case "y", "yes", "si": - return true + m, _ := db.Map() + for k := range m { + fmt.Println(k) + } + return nil + }, } - return false } diff --git a/cmd/apizza_test.go b/cmd/apizza_test.go index dbf6798..dfc5d97 100644 --- a/cmd/apizza_test.go +++ b/cmd/apizza_test.go @@ -11,6 +11,8 @@ import ( "testing" "github.com/harrybrwn/apizza/cmd/cli" + "github.com/harrybrwn/apizza/cmd/commands" + "github.com/harrybrwn/apizza/cmd/internal" "github.com/harrybrwn/apizza/cmd/internal/cmdtest" "github.com/harrybrwn/apizza/pkg/config" "github.com/harrybrwn/apizza/pkg/errs" @@ -18,18 +20,19 @@ import ( ) func TestRunner(t *testing.T) { + tests.InitHelpers(t) app := CreateApp(cmdtest.TempDB(), &cli.Config{}, nil) builder := cmdtest.NewRecorder() builder.ConfigSetup([]byte(cmdtest.TestConfigjson)) tsts := []func(*testing.T){ - cli.WithCmds(testOrderNew, NewCartCmd(builder), newAddOrderCmd(builder)), - cli.WithCmds(testAddOrder, NewCartCmd(builder), newAddOrderCmd(builder)), - cli.WithCmds(testOrderNewErr, newAddOrderCmd(builder)), - cli.WithCmds(testOrderRunAdd, NewCartCmd(builder)), - withCartCmd(builder, testOrderPriceOutput), - withCartCmd(builder, testAddToppings), - withCartCmd(builder, testOrderRunDelete), + // cli.WithCmds(testOrderNew, NewCartCmd(builder), newAddOrderCmd(builder)), + // cli.WithCmds(testAddOrder, commandsNewCartCmd(builder), newAddOrderCmd(builder)), + // cli.WithCmds(testOrderNewErr, newAddOrderCmd(builder)), + // cli.WithCmds(testOrderRunAdd, NewCartCmd(builder)), + // withCartCmd(builder, testOrderPriceOutput), + // withCartCmd(builder, testAddToppings), + // withCartCmd(builder, testOrderRunDelete), withAppCmd(testAppRootCmdRun, app), } @@ -38,9 +41,7 @@ func TestRunner(t *testing.T) { } builder.CleanUp() - if err := app.db.Destroy(); err != nil { - t.Error(err) - } + tests.Check(app.db.Destroy()) msg := senderr(errs.New("this is an error"), "error message", 4) if msg.Code != 4 { @@ -50,53 +51,38 @@ func TestRunner(t *testing.T) { func testAppRootCmdRun(t *testing.T, buf *bytes.Buffer, a *App) { a.Cmd().ParseFlags([]string{}) - if err := a.Run(a.Cmd(), []string{}); err != nil { - t.Error(err) - } + tests.Check(a.Run(a.Cmd(), []string{})) if buf.String() != a.Cmd().UsageString() { t.Error("wrong output") } - a.Cmd().ParseFlags([]string{"--test"}) - if err := a.Run(a.Cmd(), []string{}); err != nil { - t.Error(err) - } + test = true + tests.Check(a.Run(a.Cmd(), []string{})) test = false buf.Reset() - err := a.prerun(a.Cmd(), []string{}) - if err != nil { - t.Error("should not return an error") - } - err = a.postrun(a.Cmd(), []string{}) - if err != nil { - t.Error("should not return an error") - } + tests.Check(a.prerun(a.Cmd(), []string{})) + tests.Check(a.postrun(a.Cmd(), []string{})) if len(a.Cmd().Commands()) != 0 { t.Error("should not have commands yet") } - err = a.Cmd().Execute() - if err != nil { - t.Error(err) - } + tests.Check(a.Cmd().Execute()) } func TestAppResetFlag(t *testing.T) { + tests.InitHelpers(t) r := cmdtest.NewRecorder() a := CreateApp(r.ToApp()) r.ConfigSetup([]byte(cmdtest.TestConfigjson)) - a.Cmd().ParseFlags([]string{"--clear-cache"}) a.gOpts.ClearCache = true test = false - if err := a.Run(a.Cmd(), []string{}); err != nil { - t.Error(err) - } + tests.Check(a.Run(a.Cmd(), []string{})) if _, err := os.Stat(a.DB().Path()); os.IsExist(err) { - t.Error("database should not exitst") + t.Error("database should not exist") } else if !os.IsNotExist(err) { - t.Error("database should not exitst") + t.Error("database should not exist") } r.Compare(t, fmt.Sprintf("removing %s\n", a.DB().Path())) r.ClearBuf() @@ -132,19 +118,6 @@ func withAppCmd(f func(*testing.T, *bytes.Buffer, *App), c cli.CliCommand) func( } } -func withCartCmd( - b cli.Builder, - f func(*cartCmd, *bytes.Buffer, *testing.T), -) func(*testing.T) { - return func(t *testing.T) { - cart := NewCartCmd(b).(*cartCmd) - buf := &bytes.Buffer{} - cart.SetOutput(buf) - - f(cart, buf, t) - } -} - func check(e error, msg string) { if e != nil { fmt.Printf("test setup failed: %s - %s\n", e, msg) @@ -153,6 +126,8 @@ func check(e error, msg string) { } func TestExecute(t *testing.T) { + tests.InitHelpers(t) + commands.Color = false var ( exp string err error @@ -167,7 +142,7 @@ func TestExecute(t *testing.T) { test func(*testing.T) cleanup bool }{ - {args: []string{"config", "-f"}, outfunc: func() string { return fmt.Sprintf("setting up config file at %s\n%s\n", config.File(), config.File()) }}, + {args: []string{"config", "-f"}, outfunc: func() string { return fmt.Sprintf("%s\n", config.File()) }}, {args: []string{"--delete-menu", "config", "-d"}, outfunc: func() string { return config.Folder() + "\n" }}, {args: []string{"--service=Delivery", "config", "-f"}, outfunc: func() string { return config.File() + "\n" }}, {args: []string{"--log=log.txt", "config", "-d"}, outfunc: func() string { return config.Folder() + "\n" }, @@ -201,33 +176,28 @@ func TestExecute(t *testing.T) { {args: []string{"cart"}, exp: "No_orders_saved.\n"}, {args: []string{"cart", "new", "testorder", "-p=12SCREEN"}, exp: ""}, {args: []string{"cart"}, exp: "Your Orders:\n testorder\n"}, - {args: []string{"-L"}, exp: "1300 L St Nw\nWashington, DC 20005\nALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up\n\nStore id: 4336\nCoordinates: 38.9036, -77.03\n"}, + // {args: []string{"-L"}, exp: "1300 L St Nw\nWashington, DC 20005\nALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up\n\nStore id: 4336\nCoordinates: 38.9036, -77.03\n"}, + {args: []string{"-L"}, exp: "1300 L St Nw\nWashington, DC 20005\nPlease consider tipping your driver for awesome service!!!\n\nStore id: 4336\nCoordinates: 38.9036, -77.03\n"}, {args: []string{"config", "-d"}, outfunc: func() string { return config.Folder() + "\n" }, cleanup: true}, } for i, tc := range tt { buf, err = tests.CaptureOutput(func() { - errmsg = Execute(tc.args, ".apizza/.tests") + errmsg = Execute(tc.args, ".config/apizza/.tests") }) - if err != nil { - t.Error(err) - } + tests.Check(err) if errmsg != nil { - t.Error(errmsg.Msg, errmsg.Err) + t.Error(errmsg.Msg, errmsg.Err, tc.args) } - if tc.outfunc != nil { exp = tc.outfunc() } else { exp = tc.exp } - if tc.test != nil { t.Run(fmt.Sprintf("Exec test: %d", i), tc.test) } - tests.Compare(t, buf.String(), exp) - config.Save() if tc.cleanup { os.RemoveAll(config.Folder()) @@ -236,44 +206,34 @@ func TestExecute(t *testing.T) { } func TestYesOrNo(t *testing.T) { + tests.InitHelpers(t) var res bool = false f, err := ioutil.TempFile("", "") - if err != nil { - t.Fatal(err) - } - if _, err = f.Write([]byte("yes")); err != nil { - t.Fatal(err) - } - if _, err = f.Seek(0, os.SEEK_SET); err != nil { - t.Fatal(err) - } - if yesOrNo(f, "this is a message") { + tests.Fatal(err) + _, err = f.Write([]byte("yes")) + tests.Fatal(err) + _, err = f.Seek(0, os.SEEK_SET) + tests.Fatal(err) + if internal.YesOrNo(f, "this is a message") { res = true } if !res { t.Error("should have been yes") } - if err = f.Close(); err != nil { - t.Error(err) - } - if f, err = ioutil.TempFile("", ""); err != nil { - t.Fatal(err) - } - if _, err = f.Write([]byte("no")); err != nil { - t.Fatal(err) - } - if _, err = f.Seek(0, os.SEEK_SET); err != nil { - t.Fatal(err) - } + tests.Check(f.Close()) + f, err = ioutil.TempFile("", "") + tests.Check(err) + _, err = f.Write([]byte("no")) + tests.Check(err) + _, err = f.Seek(0, os.SEEK_SET) + tests.Check(err) res = false - if yesOrNo(f, "msg") { + if internal.YesOrNo(f, "msg") { res = true } if res { t.Error("should have gotten a no") } - if err = f.Close(); err != nil { - t.Error(err) - } + tests.Check(f.Close()) } diff --git a/cmd/app.go b/cmd/app.go index ec95ab9..04f3b35 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -10,6 +10,7 @@ import ( "github.com/harrybrwn/apizza/cmd/cli" "github.com/harrybrwn/apizza/cmd/client" + "github.com/harrybrwn/apizza/cmd/internal" "github.com/harrybrwn/apizza/cmd/internal/data" "github.com/harrybrwn/apizza/cmd/internal/obj" "github.com/harrybrwn/apizza/cmd/opts" @@ -47,6 +48,9 @@ func NewApp(out io.Writer) *App { } app.CliCommand = cli.NewCommand("apizza", "Dominos pizza from the command line.", app.Run) app.StoreFinder = client.NewStoreGetterFunc(app.getService, app.Address) + cmd := app.Cmd() + cmd.PersistentPreRunE = app.prerun + cmd.PostRunE = app.postrun app.SetOutput(out) return app } @@ -75,7 +79,7 @@ func (a *App) SetConfig(dir string) error { // InitDB for the app. func (a *App) InitDB() (err error) { - a.db, err = data.NewDatabase() + a.db, err = data.OpenDatabase() return } @@ -99,9 +103,28 @@ func (a *App) Address() dawg.Address { if a.addr != nil { return a.addr } + if a.conf.DefaultAddressName != "" { + addr, err := a.getDBAddress(a.conf.DefaultAddressName) + if err != nil { + fmt.Fprintf(os.Stderr, + "Warning: could not find an address named '%s'\n", + a.conf.DefaultAddressName) + if obj.AddrIsEmpty(&a.conf.Address) { + errs.StopNow(internal.ErrNoAddress, "Error", 1) + } + return &a.conf.Address + } + a.addr = addr + return addr + } return &a.conf.Address } +// GlobalOptions returns the variables for the app's global flags +func (a *App) GlobalOptions() *opts.CliFlags { + return &a.gOpts +} + // Cleanup cleans everything up. func (a *App) Cleanup() (err error) { return errs.Pair(a.db.Close(), config.Save()) @@ -132,8 +155,7 @@ func (a *App) Run(cmd *cobra.Command, args []string) (err error) { if a.opts.StoreLocation { store := a.Store() a.Println(store.Address) - a.Printf("\n") - a.Println("Store id:", store.ID) + a.Println("\nStore id:", store.ID) a.Printf("Coordinates: %s, %s\n", store.StoreCoords["StoreLatitude"], store.StoreCoords["StoreLongitude"], @@ -161,9 +183,6 @@ func (a *App) initflags() { flags := cmd.Flags() persistflags := cmd.PersistentFlags() - cmd.PersistentPreRunE = a.prerun - cmd.PostRunE = a.postrun - a.gOpts.Install(persistflags) a.opts.Install(flags) @@ -177,17 +196,28 @@ func (a *App) prerun(*cobra.Command, []string) (err error) { } var e error if a.gOpts.Address != "" { - parsed, err := dawg.ParseAddress(a.gOpts.Address) - if err != nil { - return err + // First look in the database as if the flag was a named address. + // Else check if the flag is a parsable address. + if a.db.WithBucket("addresses").Exists(a.gOpts.Address) { + newaddr, err := a.getDBAddress(a.gOpts.Address) + if err != nil { + return err + } + a.addr = newaddr + } else { + parsed, err := dawg.ParseAddress(a.gOpts.Address) + if err != nil { + return err + } + a.addr = obj.FromAddress(parsed) } - a.conf.Address = *obj.FromAddress(parsed) } if a.gOpts.Service != "" { if !(a.gOpts.Service == dawg.Delivery || a.gOpts.Service == dawg.Carryout) { return dawg.ErrBadService } + // BUG: setting the config field will implicitly change the config file a.conf.Service = a.gOpts.Service } @@ -205,6 +235,14 @@ func (a *App) prerun(*cobra.Command, []string) (err error) { return errs.Pair(err, e) } +func (a *App) getDBAddress(key string) (*obj.Address, error) { + raw, err := a.db.WithBucket("addresses").Get(key) + if err != nil { + return nil, err + } + return obj.FromGob(raw) +} + func (a *App) postrun(*cobra.Command, []string) (err error) { if a.logf != nil { return a.logf.Close() diff --git a/cmd/cart.go b/cmd/cart.go deleted file mode 100644 index e2bd5a8..0000000 --- a/cmd/cart.go +++ /dev/null @@ -1,413 +0,0 @@ -// Copyright © 2019 Harrison Brown harrybrown98@gmail.com -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cmd - -import ( - "bytes" - "errors" - "fmt" - "log" - "os" - "strings" - - "github.com/spf13/cobra" - - "github.com/harrybrwn/apizza/cmd/cli" - "github.com/harrybrwn/apizza/cmd/client" - "github.com/harrybrwn/apizza/cmd/internal/data" - "github.com/harrybrwn/apizza/cmd/internal/obj" - "github.com/harrybrwn/apizza/cmd/internal/out" - "github.com/harrybrwn/apizza/dawg" - "github.com/harrybrwn/apizza/pkg/cache" - "github.com/harrybrwn/apizza/pkg/config" - "github.com/harrybrwn/apizza/pkg/errs" -) - -type cartCmd struct { - cli.CliCommand - data.MenuCacher - client.StoreFinder - db *cache.DataBase - - updateAddr bool - validate bool - - price bool - delete bool - verbose bool - - add []string - remove string // yes, you can only remove one thing at a time - product string - - topping bool // not actually a flag anymore -} - -func (c *cartCmd) Run(cmd *cobra.Command, args []string) (err error) { - out.SetOutput(cmd.OutOrStdout()) - if len(args) < 1 { - return data.PrintOrders(c.db, c.Output(), c.verbose) - } - - if c.topping && c.product == "" { - return errors.New("must specify an item code with '--product' to edit an order's toppings") - } else if !c.topping && c.product != "" { - c.topping = true - } - - name := args[0] - - if c.delete { - if err = c.db.Delete(data.OrderPrefix + name); err != nil { - return err - } - c.Printf("%s successfully deleted.\n", name) - return nil - } - - var order *dawg.Order - if order, err = data.GetOrder(name, c.db); err != nil { - return err - } - - if c.validate { - c.Printf("validating order '%s'...\n", order.Name()) - return onlyFailures(order.Validate()) - } - - if c.updateAddr { - return c.syncWithConfig(order) - } - - if len(c.remove) > 0 { - if c.topping { - for _, p := range order.Products { - if _, ok := p.Options()[c.remove]; ok || p.Code == c.product { - delete(p.Opts, c.remove) - break - } - } - } else { - if err = order.RemoveProduct(c.remove); err != nil { - return err - } - } - return data.SaveOrder(order, c.Output(), c.db) - } - - if len(c.add) > 0 { - if c.topping { - for _, top := range c.add { - p := getOrderItem(order, c.product) - if p == nil { - return fmt.Errorf("cannot find '%s' in the '%s' order", c.product, order.Name()) - } - - err = addTopping(top, p) - if err != nil { - return err - } - } - } else { - if err := c.db.UpdateTS("menu", c); err != nil { - return err - } - menu := c.Menu() - var itm dawg.Item - for _, newP := range c.add { - itm, err = menu.GetVariant(newP) - err = errs.Pair(err, order.AddProduct(itm)) - if err != nil { - return err - } - } - } - return data.SaveOrder(order, c.Output(), c.db) - } - return out.PrintOrder(order, true, c.price) -} - -func (c *cartCmd) syncWithConfig(o *dawg.Order) error { - addr := config.Get("address").(obj.Address) - if obj.AddrIsEmpty(&addr) { - return errs.New("no address in config file") - } - - o.Address = dawg.StreetAddrFromAddress(&addr) - o.StoreID = c.Store().ID - return onlyFailures(data.SaveOrder(o, c.Output(), c.db)) -} - -func onlyFailures(e error) error { - if e == nil || dawg.IsWarning(e) { - return nil - } - return e -} - -// adds a topping. -// -// formated as :: -// name is the only one that is required. -func addTopping(topStr string, p dawg.Item) error { - var side, amount string - - topping := strings.Split(topStr, ":") - - if len(topping) < 1 { - return errors.New("incorrect topping format") - } - - if len(topping) == 1 { - side = dawg.ToppingFull - } else if len(topping) >= 2 { - side = topping[1] - } - - if len(topping) == 3 { - amount = topping[2] - } else { - amount = "1.0" - } - p.AddTopping(topping[0], side, amount) - return nil -} - -func getOrderItem(order *dawg.Order, code string) dawg.Item { - for _, itm := range order.Products { - if itm.ItemCode() == code { - return itm - } - } - return nil -} - -// NewCartCmd creates a new cart command. -func NewCartCmd(b cli.Builder) cli.CliCommand { - c := &cartCmd{ - db: b.DB(), - price: false, - delete: false, - verbose: false, - topping: false, - } - - if app, ok := b.(*App); ok { - c.StoreFinder = app - } else { - c.StoreFinder = client.NewStoreGetterFunc( - func() string { return b.Config().Service }, b.Address) - } - - c.MenuCacher = data.NewMenuCacher(menuUpdateTime, b.DB(), c.Store) - c.CliCommand = b.Build("cart ", "Manage user created orders", c) - c.Cmd().Long = `The cart command gets information on all of the user -created orders.` - - c.Cmd().PersistentPreRunE = c.persistentPreRunE - // c.Cmd().PreRunE = c.preRun - - c.Flags().BoolVar(&c.updateAddr, "update-address", c.updateAddr, "update the address of an order in accordance with the address in the config file.") - c.Flags().BoolVar(&c.validate, "validate", c.validate, "send an order to the dominos order-validation endpoint.") - - c.Flags().BoolVar(&c.price, "price", c.price, "show to price of an order") - c.Flags().BoolVarP(&c.delete, "delete", "d", c.delete, "delete the order from the database") - - c.Flags().StringSliceVarP(&c.add, "add", "a", c.add, "add any number of products to a specific order") - c.Flags().StringVarP(&c.remove, "remove", "r", c.remove, "remove a product from the order") - c.Flags().StringVarP(&c.product, "product", "p", "", "give the product that will be effected by --add or --remove") - - c.Flags().BoolVarP(&c.verbose, "verbose", "v", c.verbose, "print cart verbosly") - - c.Addcmd(newAddOrderCmd(b)) - return c -} - -func (c *cartCmd) persistentPreRunE(cmd *cobra.Command, args []string) error { - if len(args) > 1 { - return errors.New("cannot handle multiple orders") - } - return nil -} - -func (c *cartCmd) preRun(cmd *cobra.Command, args []string) error { - return nil -} - -// `cart new` command -type addOrderCmd struct { - cli.CliCommand - client.StoreFinder - db *cache.DataBase - - name string - products []string - toppings []string -} - -func (c *addOrderCmd) Run(cmd *cobra.Command, args []string) (err error) { - if c.name == "" && len(args) < 1 { - return errors.New("No order name... use '--name=' or give name as an argument") - } - order := c.Store().NewOrder() - - if c.name == "" { - order.SetName(args[0]) - } else { - order.SetName(c.name) - } - - if len(c.products) > 0 { - for i, p := range c.products { - prod, err := c.Store().GetVariant(p) - if err != nil { - return err - } - if i < len(c.toppings) { - err = prod.AddTopping(c.toppings[i], dawg.ToppingFull, "1.0") - if err != nil { - return err - } - } - order.AddProduct(prod) - } - } else if len(c.toppings) > 0 { - return errors.New("cannot add just a toppings without products") - } - return data.SaveOrder(order, &bytes.Buffer{}, c.db) -} - -func newAddOrderCmd(b cli.Builder) cli.CliCommand { - c := &addOrderCmd{name: "", products: []string{}} - c.CliCommand = b.Build("new ", - "Create a new order that will be stored in the cart.", c) - c.db = b.DB() - c.StoreFinder = client.NewStoreGetter(b) - - c.Flags().StringVarP(&c.name, "name", "n", c.name, "set the name of a new order") - c.Flags().StringSliceVarP(&c.products, "products", "p", c.products, "product codes for the new order") - c.Flags().StringSliceVarP(&c.toppings, "toppings", "t", c.toppings, "toppings for the products being added") - return c -} - -type orderCmd struct { - cli.CliCommand - db *cache.DataBase - - verbose bool - track bool - - email, phone string - fname, lname string - cvv int - number string - expiration string - - logonly bool -} - -func (c *orderCmd) Run(cmd *cobra.Command, args []string) (err error) { - if len(args) < 1 { - return data.PrintOrders(c.db, c.Output(), c.verbose) - } else if len(args) > 1 { - return errors.New("cannot handle multiple orders") - } - - if c.cvv == 0 { - return errors.New("must have cvv number. (see --cvv)") - } - order, err := data.GetOrder(args[0], c.db) - if err != nil { - return err - } - - order.AddCard(dawg.NewCard( - eitherOr(c.number, config.GetString("card.number")), - eitherOr(c.expiration, config.GetString("card.expiration")), - c.cvv)) - - names := strings.Split(config.GetString("name"), " ") - order.FirstName = eitherOr(c.fname, names[0]) - order.LastName = eitherOr(c.lname, strings.Join(names[1:], " ")) - order.Email = eitherOr(c.email, config.GetString("email")) - order.Phone = eitherOr(c.phone, config.GetString("phone")) - - c.Printf("Ordering dominos for %s\n\n", strings.Replace(obj.AddressFmt(order.Address), "\n", " ", -1)) - - if c.logonly { - log.Println("logging order:", dawg.OrderToJSON(order)) - return nil - } - - if !yesOrNo(os.Stdin, "Would you like to purchase this order? (y/n)") { - return nil - } - - c.Printf("sending order '%s'...\n", order.Name()) - err = order.PlaceOrder() - // logging happens after so any data from placeorder is included - log.Println("sending order:", dawg.OrderToJSON(order)) - if err != nil { - return err - } - c.Printf("sent to %s %s\n", order.Address.LineOne(), order.Address.City()) - - if c.verbose { - if order.ServiceMethod == dawg.Delivery { - c.Printf("sent by %s to %s %s\n", order.ServiceMethod, - order.Address.LineOne(), order.Address.City()) - } else { - c.Printf("sent order for %s\n", order.ServiceMethod) - } - c.Printf("%+v\n", order) - } - return nil -} - -func eitherOr(s1, s2 string) string { - if len(s1) == 0 { - return s2 - } - return s1 -} - -// NewOrderCmd creates a new order command. -func NewOrderCmd(b cli.Builder) cli.CliCommand { - c := &orderCmd{verbose: false} - c.CliCommand = b.Build("order", "Send an order from the cart to dominos.", c) - c.db = b.DB() - c.Cmd().Long = `The order command is the final destination for an order. This is where -the order will be populated with payment information and sent off to dominos. - -The --cvv flag must be specified, and the config file will never store the -cvv. In addition to keeping the cvv safe, payment information will never be -stored the program cache with orders. -` - flags := c.Cmd().Flags() - flags.BoolVarP(&c.verbose, "verbose", "v", c.verbose, "output the order command verbosly") - - flags.StringVar(&c.phone, "phone", "", "Set the phone number that will be used for this order") - flags.StringVar(&c.email, "email", "", "Set the email that will be used for this order") - flags.StringVar(&c.fname, "first-name", "", "Set the first name that will be used for this order") - flags.StringVar(&c.fname, "last-name", "", "Set the last name that will be used for this order") - - flags.IntVar(&c.cvv, "cvv", 0, "Set the card's cvv number for this order") - flags.StringVar(&c.number, "number", "", "the card number used for orderings") - flags.StringVar(&c.expiration, "expiration", "", "the card's expiration date") - - flags.BoolVar(&c.logonly, "log-only", false, "") - flags.MarkHidden("log-only") - return c -} diff --git a/cmd/cart/cart.go b/cmd/cart/cart.go new file mode 100644 index 0000000..419d295 --- /dev/null +++ b/cmd/cart/cart.go @@ -0,0 +1,263 @@ +package cart + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "os" + "strings" + + "github.com/spf13/cobra" + + "github.com/harrybrwn/apizza/cmd/cli" + "github.com/harrybrwn/apizza/cmd/client" + "github.com/harrybrwn/apizza/cmd/internal" + "github.com/harrybrwn/apizza/cmd/internal/data" + "github.com/harrybrwn/apizza/cmd/internal/out" + "github.com/harrybrwn/apizza/cmd/opts" + "github.com/harrybrwn/apizza/dawg" + "github.com/harrybrwn/apizza/pkg/cache" +) + +// New will create a new cart +func New(b cartBuilder) *Cart { + storefinder := client.NewStoreGetterFunc(func() string { + opts := b.GlobalOptions() + if opts.Service != "" { + return opts.Service + } + return b.Config().Service + }, b.Address) + + return &Cart{ + db: b.DB(), + finder: storefinder, + out: DefaultOutput, + MenuCacher: data.NewMenuCacher( + opts.MenuUpdateTime, + b.DB(), + storefinder.Store, + ), + } +} + +type cartBuilder interface { + cli.AddrDBBuilder + cli.StateBuilder +} + +var ( + // ErrNoCurrentOrder tells when a method of the cart struct is called + // that requires the current order to be set but it cannot find one. + ErrNoCurrentOrder = errors.New("cart has no current order set") + + // ErrOrderNotFound is raised when the cart cannot find the order + // the it was asked to get. + ErrOrderNotFound = errors.New("could not find that order") + + // DefaultOutput is the cart package's default output writer. + DefaultOutput io.Writer = os.Stdout +) + +// Cart is an abstraction on the cache.DataBase struct +// representing the user's cart for persistant orders +type Cart struct { + data.MenuCacher + // CurrentOrder is only set when SetCurrentOrder is called. + // Most functions that the cart has will fail if this is nil. + CurrentOrder *dawg.Order + + db *cache.DataBase + finder client.StoreFinder + out io.Writer +} + +// SetCurrentOrder sets the order that the cart is currently working with. +func (c *Cart) SetCurrentOrder(name string) (err error) { + c.CurrentOrder, err = c.GetOrder(name) + return err +} + +// SetOutput sets the output of logging messages. +func (c *Cart) SetOutput(w io.Writer) { + c.out = w +} + +// DeleteOrder will delete an order from the database. +func (c *Cart) DeleteOrder(name string) error { + return c.db.Delete(data.OrderPrefix + name) +} + +// GetOrder will get an order from the database. +func (c *Cart) GetOrder(name string) (*dawg.Order, error) { + raw, err := c.db.Get(data.OrderPrefix + name) + if err != nil { + return nil, err + } + if len(raw) == 0 { + return nil, ErrOrderNotFound + } + order := &dawg.Order{} + order.Init() + order.SetName(name) + order.Address = dawg.StreetAddrFromAddress(c.finder.Address()) + return order, json.Unmarshal(raw, order) +} + +// Save will save the current order and reset the current order. +func (c *Cart) Save() error { + return data.SaveOrder(c.CurrentOrder, c.out, c.db) +} + +// SaveAndReset will save the order and set it to nil so that +// it is not accidentally changed. +func (c *Cart) SaveAndReset() error { + err := c.Save() + c.CurrentOrder = nil + return err +} + +// ListOrders will return a list of the orders stored in the cart. +func (c *Cart) ListOrders() ([]string, error) { + mp, err := c.db.Map() + names := make([]string, 0, len(mp)) + if err != nil { + return nil, err + } + for k := range mp { + if strings.HasPrefix(k, data.OrderPrefix) { + names = append(names, strings.ReplaceAll(k, data.OrderPrefix, "")) + } + } + return names, nil +} + +// OrdersCompletion is a cobra valide args function for getting order names. +func (c *Cart) OrdersCompletion( + cmd *cobra.Command, + args []string, + toComplete string, +) ([]string, cobra.ShellCompDirective) { + names, err := c.ListOrders() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + return names, cobra.ShellCompDirectiveNoFileComp +} + +// Validate the current order +func (c *Cart) Validate() error { + if c.CurrentOrder == nil { + return ErrNoCurrentOrder + } + fmt.Fprintf(c.out, "validating order '%s'...\n", c.CurrentOrder.Name()) + err := c.CurrentOrder.Validate() + if dawg.IsWarning(err) { + goto ValidOrder + } + if err != nil { + return err + } + +ValidOrder: + fmt.Fprintln(c.out, "Order is ok.") + return nil +} + +// PrintCurrentOrder will print out the current order. +func (c *Cart) PrintCurrentOrder(full, color, price bool) error { + out.SetOutput(c.out) + return out.PrintOrder(c.CurrentOrder, full, color, price) +} + +// UpdateAddressAndOrderID will update the current order's address and then update +// the current order's StoreID by finding the nearest store for that address. +func (c *Cart) UpdateAddressAndOrderID(currentAddr dawg.Address) error { + c.CurrentOrder.Address = dawg.StreetAddrFromAddress(currentAddr) + s, err := dawg.NearestStore(currentAddr, c.CurrentOrder.ServiceMethod) + if err != nil { + return err + } + c.CurrentOrder.StoreID = s.ID + return nil +} + +// ValidateOrder will retrieve an order from the database and validate it. +func (c *Cart) ValidateOrder(name string) error { + o, err := c.GetOrder(name) + if err != nil { + return err + } + err = o.Validate() + if dawg.IsWarning(err) { + return nil + } + return err +} + +// AddToppings will add toppings to a product in the current order. +func (c *Cart) AddToppings(product string, toppings []string) error { + if c.CurrentOrder == nil { + return ErrNoCurrentOrder + } + return addToppingsToOrder(c.CurrentOrder, product, toppings) +} + +// AddProducts adds a list of products to the current order +func (c *Cart) AddProducts(products []string) error { + if c.CurrentOrder == nil { + return ErrNoCurrentOrder + } + if err := c.db.UpdateTS("menu", c); err != nil { + return err + } + return addProducts(c.CurrentOrder, c.Menu(), products) +} + +// PrintOrders will print out all the orders saved in the database +func (c *Cart) PrintOrders(verbose bool, color string) error { + return data.PrintOrders(c.db, c.out, verbose, color) +} + +func addToppingsToOrder(o *dawg.Order, product string, toppings []string) (err error) { + if product == "" { + return errors.New("what product are these toppings being added to") + } + for _, top := range toppings { + p := getOrderItem(o, product) + if p == nil { + return fmt.Errorf("cannot find '%s' in the '%s' order", product, o.Name()) + } + + err = internal.AddTopping(top, p) + if err != nil { + return err + } + } + return nil +} + +func addProducts(o *dawg.Order, menu *dawg.Menu, products []string) (err error) { + var itm dawg.Item + for _, newP := range products { + itm, err = menu.GetVariant(newP) + if err != nil { + return err + } + err = o.AddProduct(itm) + if err != nil { + return err + } + } + return nil +} + +func getOrderItem(order *dawg.Order, code string) dawg.Item { + for _, itm := range order.Products { + if itm.ItemCode() == code { + return itm + } + } + return nil +} diff --git a/cmd/cart/cart_test.go b/cmd/cart/cart_test.go new file mode 100644 index 0000000..b2cdf84 --- /dev/null +++ b/cmd/cart/cart_test.go @@ -0,0 +1,193 @@ +package cart + +import ( + "encoding/json" + "io/ioutil" + "testing" + + "github.com/harrybrwn/apizza/cmd/internal" + "github.com/harrybrwn/apizza/cmd/internal/cmdtest" + "github.com/harrybrwn/apizza/cmd/internal/data" + "github.com/harrybrwn/apizza/dawg" + "github.com/harrybrwn/apizza/pkg/tests" +) + +var testProduct = &dawg.OrderProduct{ItemCommon: dawg.ItemCommon{ + Code: "12SCREEN", + Tags: map[string]interface{}{"DefaultToppings": "X=1,C=1"}, +}, + Opts: map[string]interface{}{}, + Qty: 1, +} + +func TestToppings(t *testing.T) { + r, cart, order := setup(t) + defer r.CleanUp() + + tests.Exp(internal.AddTopping("", testProduct)) + order.Products = []*dawg.OrderProduct{testProduct} + tests.Fatal(data.SaveOrder(order, cart.out, r.DataBase)) + tests.Fatal(cart.SetCurrentOrder(cmdtest.OrderName)) + code := testProduct.Code + + if cart.CurrentOrder == order { + t.Error("pointers are the save, cart should have gotten order from disk") + } + if cart.CurrentOrder.Products[0].Code != code { + t.Error("did not store the order correctly") + } + if _, ok := cart.CurrentOrder.Products[0].Tags["DefaultToppings"]; !ok { + t.Error("should have the default toppings") + } + + tests.Check(cart.AddToppings(testProduct.Code, []string{ + "P", + "B:left", + "Rr:riGHt:2", + "K:Full:1.5", + "Pm:LefT:2.0", + })) + + // TODO: add test cases that make sure errors are raised when bad inputs are given + + checktoppings := func(opts map[string]interface{}) { + for _, tc := range []struct { + top, side, amount string + }{ + {"P", dawg.ToppingFull, "1.0"}, + {"B", dawg.ToppingLeft, "1.0"}, + {"Rr", dawg.ToppingRight, "2.0"}, + {"K", dawg.ToppingFull, "1.5"}, + {"Pm", dawg.ToppingLeft, "2.0"}, + } { + top, ok := opts[tc.top] + if !ok { + t.Error("options should have", tc.top, "as a topping") + } + var amount string + switch topping := top.(type) { + case map[string]string: + amount, ok = topping[tc.side] + if !ok { + t.Errorf("topping side expected was %s, got %s\n", tc.side, amount) + } + case map[string]interface{}: + a, ok := topping[tc.side] + if !ok { + t.Errorf("topping side expected was %s, got %v\n", tc.side, a) + } + amount = a.(string) + default: + t.Fatal("expected a map[string]string or map[string]interface{}") + continue + } + if !ok { + t.Errorf("topping side expected was %s, got %s\n", tc.side, amount) + } else if amount != tc.amount { + t.Errorf("wrong amount; want %s, got %s\n", tc.amount, amount) + } + } + } + checktoppings(cart.CurrentOrder.Products[0].Opts) + tests.Check(cart.Validate()) + + tests.Check(cart.SaveAndReset()) + tests.Exp(cart.AddToppings(testProduct.Code, []string{"P"})) + // tests.Check(cart.AddToppingsToOrder(cmdtest.OrderName, code, []string{"P"})) + o := dawg.Order{} + bytes, err := r.DataBase.Get(data.OrderPrefix + cmdtest.OrderName) + tests.Check(err) + tests.Check(json.Unmarshal(bytes, &o)) + tests.Check(cart.ValidateOrder(cmdtest.OrderName)) + + checktoppings(o.Products[0].Opts) +} + +func TestValidate_Err(t *testing.T) { + r, cart, o := setup(t) + defer r.CleanUp() + + o.Address = &dawg.StreetAddr{} + b, err := json.Marshal(o) + tests.Check(err) + tests.Check(r.DataBase.Put(data.OrderPrefix+o.Name(), b)) + tests.Exp(cart.ValidateOrder(cmdtest.OrderName)) + tests.Exp(cart.ValidateOrder("")) + tests.Check(cart.SetCurrentOrder(cmdtest.OrderName)) + tests.Exp(cart.Validate()) + tests.Exp(cart.Save()) + if cart.CurrentOrder == nil { + t.Error("current order should not be nil") + } + orders, err := cart.ListOrders() + tests.Check(err) + if orders[0] != cmdtest.OrderName { + t.Error("did not list correct order name") + } + orders, _ = cart.OrdersCompletion(nil, []string{}, "") + if orders[0] != cmdtest.OrderName { + t.Error("did not list correct order name") + } + tests.Exp(cart.SaveAndReset()) + if cart.CurrentOrder != nil { + t.Error("current order should be nil") + } + if cart.Validate() != ErrNoCurrentOrder { + t.Error("wrong error") + } + tests.Check(cart.DeleteOrder(cmdtest.OrderName)) + _, err = cart.GetOrder(cmdtest.OrderName) + tests.Exp(err) + tests.Exp(cart.ValidateOrder(cmdtest.OrderName)) +} + +func TestProducts(t *testing.T) { + r, cart, order := setup(t) + defer r.CleanUp() + + order.Products = []*dawg.OrderProduct{} + b, err := json.Marshal(order) + tests.Check(err) + tests.Check(r.DataBase.Put(data.OrderPrefix+order.Name(), b)) + codes := []string{"12SCREEN", "W08PBBQW", "10THIN", "10SCMEATZA"} + + tests.Check(cart.SetCurrentOrder(cmdtest.OrderName)) + tests.Check(cart.AddProducts(codes)) + for i, c := range codes { + tests.StrEq(cart.CurrentOrder.Products[i].Code, c, "set wrong code") + } + tests.Check(cart.SaveAndReset()) + tests.Exp(cart.AddProducts(codes)) + + o := dawg.Order{} + bytes, err := r.DataBase.Get(data.OrderPrefix + cmdtest.OrderName) + tests.Check(err) + tests.Check(json.Unmarshal(bytes, &o)) + for i, c := range codes { + tests.StrEq(o.Products[i].Code, c, "stored wrong code") + } + tests.Check(cart.PrintOrders(false, "")) +} + +func TestHelpers_Err(t *testing.T) { + r, cart, o := setup(t) + defer r.CleanUp() + m, err := cart.finder.Store().Menu() + tests.Check(err) + if m == nil { + t.Fatal("nil menu") + } + tests.Exp(addProducts(o, m, []string{"nope", "not a thing"})) + tests.Check(addProducts(o, m, []string{"12SCREEN"})) + tests.Exp(addToppingsToOrder(o, "nothere", []string{"K", "B"})) + tests.Exp(addToppingsToOrder(o, "", []string{"K", "B"})) + tests.Exp(addToppingsToOrder(o, "12SCREEN", []string{""})) +} + +func setup(t *testing.T) (*cmdtest.Recorder, *Cart, *dawg.Order) { + tests.InitHelpers(t) + r := cmdtest.NewRecorder() + cart := New(r) + cart.SetOutput(ioutil.Discard) + return r, cart, cmdtest.NewTestOrder() +} diff --git a/cmd/cart_test.go b/cmd/cart_test.go deleted file mode 100644 index fe56375..0000000 --- a/cmd/cart_test.go +++ /dev/null @@ -1,235 +0,0 @@ -package cmd - -import ( - "bytes" - "fmt" - "strings" - "testing" - - "github.com/harrybrwn/apizza/cmd/cli" - "github.com/harrybrwn/apizza/cmd/internal/cmdtest" - "github.com/harrybrwn/apizza/pkg/tests" -) - -func testOrderNew(t *testing.T, buf *bytes.Buffer, cmds ...cli.CliCommand) { - cart, add := cmds[0], cmds[1] - add.Cmd().ParseFlags([]string{"--name=testorder", "--products=12SCMEATZA"}) - err := add.Run(add.Cmd(), []string{}) - if err != nil { - t.Error(err) - } - buf.Reset() - - if err := cart.Run(cart.Cmd(), []string{"testorder"}); err != nil { - t.Error(err) - } - expected := `testorder - products: - Medium (12") Hand Tossed MeatZZa - code: 12SCMEATZA - options: - B: 1/1 1 - C: 1/1 1.5 - H: 1/1 1 - P: 1/1 1 - S: 1/1 1 - X: 1/1 1 - quantity: 1 - storeID: 4336 - method: Carryout - address: 1600 Pennsylvania Ave NW - Washington, DC 20500 -` - tests.Compare(t, buf.String(), strings.Replace(expected, "\t", " ", -1)) -} - -func testAddOrder(t *testing.T, buf *bytes.Buffer, cmds ...cli.CliCommand) { - cart, add := cmds[0], cmds[1] - if err := add.Run(add.Cmd(), []string{"testing"}); err != nil { - t.Error(err) - } - if buf.String() != "" { - t.Errorf("wrong output: should have no output: '%s'", buf.String()) - } - buf.Reset() - cart.Cmd().ParseFlags([]string{"-d"}) - if err := cart.Run(cart.Cmd(), []string{"testing"}); err != nil { - t.Error(err) - } - buf.Reset() -} - -func testOrderNewErr(t *testing.T, buf *bytes.Buffer, cmds ...cli.CliCommand) { - if err := cmds[0].Run(cmds[0].Cmd(), []string{}); err == nil { - t.Error("expected error") - } -} - -func testOrderRunAdd(t *testing.T, buf *bytes.Buffer, cmds ...cli.CliCommand) { - cart := cmds[0] - if err := cart.Run(cart.Cmd(), []string{}); err != nil { - t.Error(err) - } - tests.Compare(t, buf.String(), "Your Orders:\n testorder\n") - buf.Reset() - cart.Cmd().ParseFlags([]string{"--add", "10SCPFEAST,PSANSAMV"}) - if err := cart.Run(cart.Cmd(), []string{"testorder"}); err != nil { - t.Error(err) - } - tests.Compare(t, buf.String(), "order successfully updated.\n") -} - -func testOrderPriceOutput(cart *cartCmd, buf *bytes.Buffer, t *testing.T) { - cart.price = true - cart.updateAddr = true - - if err := cart.Run(cart.Cmd(), []string{"testorder"}); err != nil { - t.Error(err) - } - cart.updateAddr = false - if err := cart.Run(cart.Cmd(), []string{"to-many", "args"}); err == nil { - t.Error("expected error") - } - m := cart.Menu() - m2 := cart.Menu() - if m != m2 { - t.Error("should have cached the menu") - } -} - -func testOrderRunDelete(cart *cartCmd, buf *bytes.Buffer, t *testing.T) { - cart.delete = true - if err := cart.Run(cart.Cmd(), []string{"testorder"}); err != nil { - t.Error(err) - } - tests.Compare(t, buf.String(), "testorder successfully deleted.\n") - cart.delete = false - buf.Reset() - cart.Cmd().ParseFlags([]string{}) - if err := cart.Run(cart.Cmd(), []string{}); err != nil { - t.Error(err) - } - tests.Compare(t, buf.String(), "No orders saved.\n") - buf.Reset() - if err := cart.Run(cart.Cmd(), []string{"not_a_real_order"}); err == nil { - t.Error("expected error") - } - - cart.topping = false - cart.validate = true - if err := cart.Run(cart.Cmd(), []string{}); err != nil { - t.Error(err) - } -} - -func testAddToppings(cart *cartCmd, buf *bytes.Buffer, t *testing.T) { - cart.add = []string{"10SCREEN"} - if err := cart.Run(cart.Cmd(), []string{"testorder"}); err != nil { - t.Error(err) - } - cart.add = nil - - cart.product = "10SCREEN" - cart.add = []string{"P", "K"} - cart.topping = false - if err := cart.Run(cart.Cmd(), []string{"testorder"}); err != nil { - t.Error(err) - } - - cart.product = "" - cart.add = []string{} - cart.topping = false - buf.Reset() - if err := cart.Run(cart.Cmd(), []string{"testorder"}); err != nil { - t.Error(err) - } - - expected := `Small (10") Hand Tossed Pizza - code: 10SCREEN - options: - C: 1/1 1 - K: 1/1 1.0 - P: 1/1 1.0 - X: 1/1 1 - quantity: 1` - - if !strings.Contains(buf.String(), expected) { - t.Error("bad output") - } - buf.Reset() - - cart.topping = false - cart.product = "10SCREEN" - cart.remove = "C" - if err := cart.Run(cart.Cmd(), []string{"testorder"}); err != nil { - t.Error(err) - } - buf.Reset() - cart.topping = false - cart.product = "" - cart.remove = "" - if err := cart.Run(cart.Cmd(), []string{"testorder"}); err != nil { - t.Error(err) - } - expected = ` Small (10") Hand Tossed Pizza - code: 10SCREEN - options: - C: 1/1 1 - K: 1/1 1.0 - P: 1/1 1.0 - X: 1/1 1 - quantity: 1` - if !strings.Contains(buf.String(), expected) { - fmt.Println("got:") - fmt.Println(buf.String()) - fmt.Println("expected:") - fmt.Print(expected) - t.Error("bad output") - } - buf.Reset() - - cart.topping = false - cart.remove = "10SCREEN" - if err := cart.Run(cart.Cmd(), []string{"testorder"}); err != nil { - t.Error(err) - } - if strings.Contains(buf.String(), expected) { - t.Error("bad output") - } -} - -func TestOrder(t *testing.T) { - r := cmdtest.NewRecorder() - defer r.CleanUp() - - ordercmd := NewOrderCmd(r) - err := ordercmd.Run(ordercmd.Cmd(), []string{}) - if err != nil { - t.Error(err) - } - if err = ordercmd.Run(ordercmd.Cmd(), []string{"one", "two"}); err == nil { - t.Error("expected error") - } - err = ordercmd.Run(ordercmd.Cmd(), []string{"anorder"}) - if err == nil { - t.Error("expected an error") - } - cmd := ordercmd.(*orderCmd) - cmd.cvv = 100 - if err = cmd.Run(cmd.Cmd(), []string{"nothere"}); err == nil { - t.Error("the order is not in the database") - } - cmd.cvv = 0 -} - -func TestEitherOr(t *testing.T) { - if eitherOr("one", "") != "one" { - t.Error("wrong result from 'eitherOr'") - } - if eitherOr("", "two") != "two" { - t.Error("wrong result from 'eitherOr'") - } - if eitherOr("a", "b") != "a" { - t.Error("wrong result from 'eitherOr'") - } -} diff --git a/cmd/cli/builder.go b/cmd/cli/builder.go index 3ae9686..868ecc7 100644 --- a/cmd/cli/builder.go +++ b/cmd/cli/builder.go @@ -3,6 +3,7 @@ package cli import ( "io" + "github.com/harrybrwn/apizza/cmd/opts" "github.com/harrybrwn/apizza/dawg" "github.com/harrybrwn/apizza/pkg/cache" ) @@ -11,12 +12,12 @@ import ( type Builder interface { CommandBuilder DBBuilder - ConfigBuilder + StateBuilder AddressBuilder Output() io.Writer } -// CommandBuilder defines an interface for building commnads. +// CommandBuilder defines an interface for building commands. type CommandBuilder interface { Build(use, short string, r Runner) *Command } @@ -37,6 +38,14 @@ type AddressBuilder interface { Address() dawg.Address } +// StateBuilder defines a cli builder that has control over the +// program state, whether that is from the config file or the global +// command line options. +type StateBuilder interface { + ConfigBuilder + GlobalOptions() *opts.CliFlags +} + // AddrDBBuilder is an anddress-builder and a db-builder. type AddrDBBuilder interface { CommandBuilder diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index b05d408..ce13fca 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -77,8 +77,13 @@ func (c *Command) Run(cmd *cobra.Command, args []string) error { // SetOutput sets the command output func (c *Command) SetOutput(out io.Writer) { + c.SetOut(out) +} + +// SetOut sets the command output +func (c *Command) SetOut(out io.Writer) { c.output = out - c.cmd.SetOutput(c.output) + c.cmd.SetOut(c.output) } // Output returns the command's output writer. diff --git a/cmd/cli/config.go b/cmd/cli/config.go index 4dcc561..e34a8ce 100644 --- a/cmd/cli/config.go +++ b/cmd/cli/config.go @@ -4,16 +4,18 @@ import ( "errors" "github.com/harrybrwn/apizza/cmd/internal/obj" + "github.com/harrybrwn/apizza/dawg" "github.com/harrybrwn/apizza/pkg/config" ) // Config is the configuration struct type Config struct { - Name string `config:"name" json:"name"` - Email string `config:"email" json:"email"` - Phone string `config:"phone" json:"phone"` - Address obj.Address `config:"address" json:"address"` - Card struct { + Name string `config:"name" json:"name"` + Email string `config:"email" json:"email"` + Phone string `config:"phone" json:"phone"` + Address obj.Address `config:"address" json:"address" yaml:"address,omitempty"` + DefaultAddressName string `config:"default-address-name" json:"default-address-name" yaml:"default-address-name"` + Card struct { Number string `config:"number" json:"number"` Expiration string `config:"expiration" json:"expiration"` } `config:"card" json:"card"` @@ -28,7 +30,7 @@ func (c *Config) Get(key string) interface{} { // Set a config variable func (c *Config) Set(key string, val interface{}) error { if config.FieldName(c, key) == "Service" { - if val != "Delivery" && val != "Carryout" { + if val != dawg.Delivery && val != dawg.Carryout { return errors.New("service must be either 'Delivery' or 'Carryout'") } } diff --git a/cmd/client/client.go b/cmd/client/client.go index c1cf5ac..8465cd6 100644 --- a/cmd/client/client.go +++ b/cmd/client/client.go @@ -7,6 +7,8 @@ import ( "github.com/harrybrwn/apizza/cmd/internal/data" ) +// TODO: this has a terrible name, in fact the whole package needs renaming + // Client defines an interface which interacts with the dominos api. type Client interface { StoreFinder diff --git a/cmd/client/storegetter.go b/cmd/client/storegetter.go index eb0d932..17b0821 100644 --- a/cmd/client/storegetter.go +++ b/cmd/client/storegetter.go @@ -2,6 +2,7 @@ package client import ( "github.com/harrybrwn/apizza/cmd/cli" + "github.com/harrybrwn/apizza/cmd/internal" "github.com/harrybrwn/apizza/cmd/internal/obj" "github.com/harrybrwn/apizza/dawg" "github.com/harrybrwn/apizza/pkg/errs" @@ -10,7 +11,11 @@ import ( // StoreFinder is a mixin that allows for efficient caching and retrival of // store structs. type StoreFinder interface { + // Store will return a dominos store Store() *dawg.Store + + // Address() will return the address of the delivery location NOT the store address. + cli.AddressBuilder } // storegetter is meant to be a mixin for any struct that needs to be able to @@ -46,12 +51,16 @@ func (s *storegetter) Store() *dawg.Store { var err error var address = s.getaddr() if obj.AddrIsEmpty(address) { - errs.Handle(errs.New("no address given in config file or as flag"), "Error", 1) + errs.StopNow(errs.New(internal.ErrNoAddress), "Error", 1) } s.dstore, err = dawg.NearestStore(address, s.getmethod()) if err != nil { - errs.Handle(err, "Store Find Error", 1) // will exit + errs.StopNow(err, "Store Find Error", 1) // will exit } } return s.dstore } + +func (s *storegetter) Address() dawg.Address { + return s.getaddr() +} diff --git a/cmd/commands/address.go b/cmd/commands/address.go new file mode 100644 index 0000000..b10bcd4 --- /dev/null +++ b/cmd/commands/address.go @@ -0,0 +1,121 @@ +package commands + +import ( + "bufio" + "fmt" + "io" + "strings" + + "github.com/harrybrwn/apizza/cmd/cli" + "github.com/harrybrwn/apizza/cmd/internal/obj" + "github.com/harrybrwn/apizza/pkg/cache" + "github.com/spf13/cobra" +) + +// NewAddAddressCmd creates the 'add-address' command. +func NewAddAddressCmd(b cli.Builder, in io.Reader) cli.CliCommand { + c := &addAddressCmd{ + db: b.DB(), + in: in, + new: false, + } + c.CliCommand = b.Build("address", "Add a new named address to the internal storage.", c) + cmd := c.Cmd() + cmd.Long = `The address command is where user addresses are managed. Addresses added with +the '--new' flag are put into the program's internal storage. To set one of theses +addresses as the program default set the appropriate config file option (default-address-name).` + cmd.Aliases = []string{"addr"} + cmd.Flags().BoolVarP(&c.new, "new", "n", c.new, "add a new address") + cmd.Flags().StringVarP(&c.delete, "delete", "d", "", "delete an address") + return c +} + +type addAddressCmd struct { + cli.CliCommand + + db *cache.DataBase + in io.Reader + new bool + delete string +} + +func (a *addAddressCmd) Run(cmd *cobra.Command, args []string) error { + if a.new { + return a.newAddress() + } + if a.delete != "" { + db := a.db.WithBucket("addresses") + return db.Delete(a.delete) + } + + m, err := a.db.WithBucket("addresses").Map() + if err != nil { + return err + } + + if len(m) == 0 { + a.Println("No addresses stored (see '--new' flag)") + return nil + } + + var addr *obj.Address + for key, val := range m { + addr, err = obj.FromGob(val) + if err != nil { + return err + } + + a.Printf("%s:\n %s\n", key, obj.AddressFmtIndent(addr, 2)) + } + return nil +} + +type reader struct { + scanner *bufio.Reader +} + +func (a *addAddressCmd) newAddress() error { + r := reader{bufio.NewReader(a.in)} + addr := obj.Address{} + + a.Printf("Address Name: ") + name, err := r.readline() + if err != nil { + return err + } + a.Printf("Street Address: ") + addr.Street, err = r.readline() + if err != nil { + return err + } + a.Printf("City: ") + addr.CityName, err = r.readline() + if err != nil { + return err + } + a.Printf("State Code: ") + addr.State, err = r.readline() + if err != nil { + return err + } + a.Printf("Zipcode: ") + addr.Zipcode, err = r.readline() + if err != nil { + return err + } + + fmt.Fprint(a.Output(), name, ":\n", addr, "\n") + raw, err := obj.AsGob(&addr) + if err != nil { + return err + } + return a.db.WithBucket("addresses").Put(name, raw) +} + +func (r *reader) readline() (string, error) { + lineone, err := r.scanner.ReadString('\n') + if err != nil { + return "", err + } + return strings.Trim(lineone, "\n \t\r"), nil +} diff --git a/cmd/commands/cart.go b/cmd/commands/cart.go new file mode 100644 index 0000000..a7f22e4 --- /dev/null +++ b/cmd/commands/cart.go @@ -0,0 +1,368 @@ +package commands + +import ( + "bytes" + "errors" + "fmt" + "log" + "os" + "strings" + + "github.com/harrybrwn/apizza/cmd/cart" + "github.com/harrybrwn/apizza/cmd/cli" + "github.com/harrybrwn/apizza/cmd/client" + "github.com/harrybrwn/apizza/cmd/internal" + "github.com/harrybrwn/apizza/cmd/internal/data" + "github.com/harrybrwn/apizza/cmd/internal/obj" + "github.com/harrybrwn/apizza/dawg" + "github.com/harrybrwn/apizza/pkg/cache" + "github.com/harrybrwn/apizza/pkg/config" + "github.com/spf13/cobra" +) + +// NewCartCmd creates a new cart command. +func NewCartCmd(b cli.Builder) cli.CliCommand { + c := &cartCmd{ + cart: cart.New(b), + price: false, + delete: false, + verbose: false, + topping: false, + color: Color, + getaddress: b.Address, + } + + c.CliCommand = b.Build("cart ", "Manage user created orders", c) + cmd := c.Cmd() + + cmd.Long = `The cart command gets information on and edit all of the user +created orders.` + + cmd.PreRunE = func(cmd *cobra.Command, args []string) error { + if len(args) > 1 { + return errors.New("cannot handle multiple orders") + } + return nil + } + cmd.ValidArgsFunction = c.cart.OrdersCompletion + + c.Flags().BoolVar(&c.validate, "validate", c.validate, "Send an order to the dominos order-validation endpoint") + c.Flags().BoolVar(&c.price, "price", c.price, "Show to price of an order") + c.Flags().BoolVarP(&c.delete, "delete", "d", c.delete, "Delete the order from the database") + + c.Flags().StringSliceVarP(&c.add, "add", "a", c.add, "Add any number of products to a specific order") + c.Flags().StringVarP(&c.remove, "remove", "r", c.remove, "Remove a product from the order") + c.Flags().StringVarP(&c.product, "product", "p", "", "Give the product that will be effected by --add or --remove") + + c.Flags().BoolVarP(&c.verbose, "verbose", "v", c.verbose, "Print cart verbosely") + + c.Addcmd(newAddOrderCmd(b)) + return c +} + +// `apizza cart` +type cartCmd struct { + cli.CliCommand + cart *cart.Cart + + validate bool + price bool + delete bool + verbose bool + color bool + + add []string + remove string // yes, you can only remove one thing at a time + product string + + topping bool // not actually a flag anymore + getaddress func() dawg.Address +} + +func (c *cartCmd) Run(cmd *cobra.Command, args []string) (err error) { + c.cart.SetOutput(c.Output()) + if len(args) < 1 { + var colstr string + if c.color { + colstr = "\033[01;34m" + } + return c.cart.PrintOrders(c.verbose, colstr) + } + + if c.topping && c.product == "" { + return errors.New("must specify an item code with '--product' to edit an order's toppings") + } else if !c.topping && c.product != "" { + c.topping = true + } + + name := args[0] + + if c.delete { + if err = c.cart.DeleteOrder(name); err != nil { + return err + } + c.Printf("%s successfully deleted.\n", name) + return nil + } + // Set the order that will be used will the cart functions + if err = c.cart.SetCurrentOrder(name); err != nil { + return err + } + + var order *dawg.Order = c.cart.CurrentOrder + + if !order.Address.Equal(c.getaddress()) { + if err = c.cart.UpdateAddressAndOrderID(c.getaddress()); err != nil { + return err + } + } + + if c.validate { + // validate the current order and stop + return c.cart.Validate() + } + + if len(c.remove) > 0 { + if c.topping { + for _, p := range order.Products { + if _, ok := p.Options()[c.remove]; ok || p.Code == c.product { + delete(p.Opts, c.remove) + break + } + } + } else { + if err = order.RemoveProduct(c.remove); err != nil { + return err + } + } + return c.cart.SaveAndReset() + } + + if len(c.add) > 0 { + if c.topping { + err = c.cart.AddToppings(c.product, c.add) + } else { + err = c.cart.AddProducts(c.add) + } + if err != nil { + return err + } + // save order and return early before order is printed out + return c.cart.SaveAndReset() + } + return c.cart.PrintCurrentOrder(true, c.color, c.price) +} + +func newAddOrderCmd(b cli.Builder) cli.CliCommand { + c := &addOrderCmd{name: "", product: ""} + c.CliCommand = b.Build("new ", + "Create a new order that will be stored in the cart.", c) + c.db = b.DB() + c.StoreFinder = client.NewStoreGetter(b) + + c.Flags().StringVarP(&c.name, "name", "n", c.name, "set the name of a new order") + c.Flags().StringVarP(&c.product, "product", "p", c.product, "product codes for the new order") + c.Flags().StringSliceVarP(&c.toppings, "toppings", "t", c.toppings, "toppings for the products being added") + return c +} + +// `apizza cart new` command +type addOrderCmd struct { + cli.CliCommand + client.StoreFinder + db *cache.DataBase + + name string + product string + toppings []string +} + +func (c *addOrderCmd) Run(cmd *cobra.Command, args []string) (err error) { + if c.name == "" && len(args) < 1 { + return internal.ErrNoOrderName + } + order := c.Store().NewOrder() + + if c.name == "" { + order.SetName(args[0]) + } else { + order.SetName(c.name) + } + + // User interface options: + // - only add one product but a list of toppings + // - add a list of products in parallel with a list of toppings (vectorized approach) + // - add some weird extra syntax to do both (bad idea) + if c.product != "" { + prod, err := c.Store().GetVariant(c.product) + if err != nil { + return err + } + for _, t := range c.toppings { + if err = internal.AddTopping(t, prod); err != nil { + return err + } + } + if err = order.AddProduct(prod); err != nil { + return err + } + } else if len(c.toppings) > 0 { + return errors.New("cannot add just a toppings without products") + } + return data.SaveOrder(order, &bytes.Buffer{}, c.db) +} + +// NewOrderCmd creates a new order command. +func NewOrderCmd(b cli.Builder) cli.CliCommand { + c := &orderCmd{ + verbose: false, + color: Color, + getaddress: b.Address, + } + c.CliCommand = b.Build("order", "Send an order from the cart to dominos.", c) + c.db = b.DB() + c.Cmd().Long = `The order command is the final destination for an order. This is where +the order will be populated with payment information and sent off to dominos. + +The --cvv flag must be specified, and the config file will never store the +cvv. In addition to keeping the cvv safe, payment information will never be +stored the program cache with orders. +` + c.Cmd().PreRunE = func(cmd *cobra.Command, args []string) error { + if len(args) > 1 { + return errors.New("cannot handle multiple orders") + } + return nil + } + + flags := c.Cmd().Flags() + flags.BoolVarP(&c.verbose, "verbose", "v", c.verbose, "output the order command verbosely") + + flags.StringVar(&c.phone, "phone", "", "Set the phone number that will be used for this order") + flags.StringVar(&c.email, "email", "", "Set the email that will be used for this order") + flags.StringVar(&c.fname, "first-name", "", "Set the first name that will be used for this order") + flags.StringVar(&c.fname, "last-name", "", "Set the last name that will be used for this order") + + flags.IntVar(&c.cvv, "cvv", 0, "Set the card's cvv number for this order") + flags.StringVar(&c.number, "number", "", "the card number used for orderings") + flags.StringVar(&c.expiration, "expiration", "", "the card's expiration date") + + flags.BoolVarP(&c.yes, "yes", "y", c.yes, "do not prompt the user with a question") + flags.BoolVar(&c.logonly, "log-only", false, "") + flags.MarkHidden("log-only") + return c +} + +// `apizza order` +type orderCmd struct { + cli.CliCommand + db *cache.DataBase + + verbose bool + track bool + + email, phone string + fname, lname string + cvv int + number string + expiration string + yes bool + color bool + + logonly bool + getaddress func() dawg.Address +} + +func (c *orderCmd) Run(cmd *cobra.Command, args []string) (err error) { + if len(args) < 1 { + var colorstr string + if c.color { + colorstr = "\033[01;34m" + } + return data.PrintOrders(c.db, c.Output(), c.verbose, colorstr) + } else if len(args) > 1 { + return errors.New("cannot handle multiple orders") + } + + if c.cvv == 0 { + return errors.New("must have cvv number. (see --cvv)") + } + order, err := data.GetOrder(args[0], c.db) + if err != nil { + return err + } + + num := eitherOr(c.number, config.GetString("card.number")) + exp := eitherOr(c.expiration, config.GetString("card.expiration")) + if num == "" { + return errors.New("no card number given") + } + if exp == "" { + return errors.New("no card expiration date given") + } + + card := dawg.NewCard(num, exp, c.cvv) + if err = dawg.ValidateCard(card); err != nil { + return err + } + order.AddCard(card) + + names := strings.Split(config.GetString("name"), " ") + if len(names) >= 1 { + order.FirstName = eitherOr(c.fname, names[0]) + } + if len(names) >= 2 { + order.LastName = eitherOr(c.lname, strings.Join(names[1:], " ")) + } + order.Email = eitherOr(c.email, config.GetString("email")) + order.Phone = eitherOr(c.phone, config.GetString("phone")) + + if !order.Address.Equal(c.getaddress()) { + order.Address = dawg.StreetAddrFromAddress(c.getaddress()) + s, err := dawg.NearestStore(c.getaddress(), order.ServiceMethod) + if err != nil { + return err + } + order.StoreID = s.ID + } + + c.Printf("Ordering dominos for %s to %s\n\n", order.ServiceMethod, strings.Replace(obj.AddressFmt(order.Address), "\n", " ", -1)) + + if c.logonly { + log.Println("logging order:", dawg.OrderToJSON(order)) + return nil + } + + if !c.yes { + if !internal.YesOrNo(os.Stdin, "Would you like to purchase this order? (y/n)") { + return nil + } + } + + c.Printf("sending order '%s'...\n", order.Name()) + err = order.PlaceOrder() + // logging happens after so any data from placeorder is included + log.Println("sending order:", dawg.OrderToJSON(order)) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error()) + } + c.Printf("sent to %s %s\n", order.Address.LineOne(), order.Address.City()) + + if c.verbose { + if order.ServiceMethod == dawg.Delivery { + c.Printf("sent by %s to %s %s\n", order.ServiceMethod, + order.Address.LineOne(), order.Address.City()) + } else { + c.Printf("sent order for %s\n", order.ServiceMethod) + } + c.Printf("%+v\n", order) + } + return nil +} + +func eitherOr(s1, s2 string) string { + if len(s1) == 0 { + return s2 + } + return s1 +} diff --git a/cmd/commands/cart_test.go b/cmd/commands/cart_test.go new file mode 100644 index 0000000..afdf28d --- /dev/null +++ b/cmd/commands/cart_test.go @@ -0,0 +1,317 @@ +package commands + +import ( + "bytes" + "fmt" + "strings" + "testing" + + "github.com/harrybrwn/apizza/cmd/cli" + "github.com/harrybrwn/apizza/cmd/internal/cmdtest" + "github.com/harrybrwn/apizza/cmd/internal/obj" + "github.com/harrybrwn/apizza/pkg/errs" + "github.com/harrybrwn/apizza/pkg/tests" +) + +func init() { + Color = false +} + +func addTestOrder(b cli.Builder) { + new := newAddOrderCmd(b).Cmd() + if err := errs.Pair( + new.ParseFlags([]string{"--name=testorder", "--product=14SCREEN", "--toppings=P,K"}), + new.RunE(new, []string{}), + ); err != nil { + panic("could not add a test order: " + err.Error()) + } +} + +func newTestCart(b cli.Builder) *cartCmd { + cart := NewCartCmd(b) + addTestOrder(b) + return cart.(*cartCmd) +} + +func TestCartCommand(t *testing.T) { + b := cmdtest.NewTestRecorder(t) + defer b.CleanUp() + cart := newTestCart(b) + + tests.Check(cart.Run(cart.Cmd(), []string{"testorder"})) + if !strings.Contains(b.Out.String(), "testorder") { + t.Error("cart output did not have the right name") + } + if !strings.Contains(b.Out.String(), "14SCREEN") { + t.Error("does not have the correct product") + } + // tests.Exp(cart.Run(cart.Cmd(), []string{"testorder", "another_order"})) + b.Conf.Address = obj.Address{ + Street: "600 Mountain Ave bldg 5", + CityName: "New Providence", + State: "NJ", + Zipcode: "07974", + } + tests.Check(cart.Run(cart.Cmd(), []string{"testorder"})) + cart.Cmd().ParseFlags([]string{"--validate"}) + tests.Check(cart.Run(cart.Cmd(), []string{"testorder"})) + tests.Exp(cart.Cmd().PreRunE(cart.Cmd(), []string{"testorder", "another"})) + tests.Check(cart.Cmd().PreRunE(cart.Cmd(), []string{"testorder"})) + + b.Out.Reset() + cart.validate = false + cart.Cmd().ParseFlags([]string{"--add=K"}) + tests.Exp(cart.Run(cart.Cmd(), []string{"testorder"})) + fmt.Println(b.Out.String()) +} + +func TestCartToppings(t *testing.T) { + b := cmdtest.NewTestRecorder(t) + defer b.CleanUp() + cart := newTestCart(b) + tests.Check(cart.Cmd().ParseFlags([]string{"-a=P:left:1.5", "-p=14SCREEN"})) + tests.Check(cart.Run(cart.Cmd(), []string{"testorder"})) +} + +func TestCartToppings_Err(t *testing.T) { + b := cmdtest.NewTestRecorder(t) + defer b.CleanUp() + cart := newTestCart(b) + tests.Check(cart.Cmd().ParseFlags([]string{"-a=P:badinput:1.5", "-p=14SCREEN"})) + tests.Exp(cart.Run(cart.Cmd(), []string{"testorder"})) +} + +func testOrderNew(t *testing.T, buf *bytes.Buffer, cmds ...cli.CliCommand) { + cart, add := cmds[0], cmds[1] + add.Cmd().ParseFlags([]string{"--name=testorder", "--product=12SCMEATZA"}) + err := add.Run(add.Cmd(), []string{}) + if err != nil { + t.Error(err) + } + buf.Reset() + + if err := cart.Run(cart.Cmd(), []string{"testorder"}); err != nil { + t.Error(err) + } + expected := `testorder + products: + Medium (12") Hand Tossed MeatZZa + code: 12SCMEATZA + options: + B: 1/1 1 + C: 1/1 1.5 + H: 1/1 1 + P: 1/1 1 + S: 1/1 1 + X: 1/1 1 + quantity: 1 + storeID: 4336 + method: Carryout + address: 1600 Pennsylvania Ave NW + Washington, DC 20500 +` + tests.Compare(t, buf.String(), strings.Replace(expected, "\t", " ", -1)) +} + +// func testAddOrder(t *testing.T, buf *bytes.Buffer, cmds ...cli.CliCommand) { +func TestAddOrder(t *testing.T) { + // cart, add := cmds[0], cmds[1] + + b := cmdtest.NewTestRecorder(t) + defer b.CleanUp() + cart := NewCartCmd(b) + add := newAddOrderCmd(b) + + tests.Check(add.Run(add.Cmd(), []string{"testing"})) + if b.Out.String() != "" { + t.Errorf("wrong output: should have no output: '%s'", b.Out.String()) + } + b.Out.Reset() + cart.Cmd().ParseFlags([]string{"-d"}) + tests.Check(cart.Run(cart.Cmd(), []string{"testing"})) + b.Out.Reset() +} + +func testOrderNewErr(t *testing.T, buf *bytes.Buffer, cmds ...cli.CliCommand) { + if err := cmds[0].Run(cmds[0].Cmd(), []string{}); err == nil { + t.Error("expected error") + } +} + +func TestOrderRunAdd(t *testing.T) { + b := cmdtest.NewTestRecorder(t) + defer b.CleanUp() + cart := newTestCart(b) + cart.color = false + tests.Check(cart.Run(cart.Cmd(), []string{})) + tests.Compare(t, b.Out.String(), "Your Orders:\n testorder\n") + b.Out.Reset() + tests.Check(cart.Cmd().ParseFlags([]string{"--add", "10SCPFEAST,PSANSAMV"})) + tests.Check(cart.Run(cart.Cmd(), []string{"testorder"})) + tests.Compare(t, b.Out.String(), "order successfully updated.\n") +} + +func TestOrderPriceOutput(t *testing.T) { + b := cmdtest.NewTestRecorder(t) + defer b.CleanUp() + cart := newTestCart(b) + cart.price = true + + tests.Check(cart.Run(cart.Cmd(), []string{"testorder"})) + tests.Exp(cart.Run(cart.Cmd(), []string{"to-many", "args"})) + m := cart.cart.Menu() + m2 := cart.cart.Menu() + if m != m2 { + t.Error("should have cached the menu") + } +} + +// func testOrderRunDelete(cart *cartCmd, buf *bytes.Buffer, t *testing.T) { +func TestOrderRunDelete(t *testing.T) { + b := cmdtest.NewTestRecorder(t) + defer b.CleanUp() + cart := newTestCart(b) + + cart.delete = true + tests.Check(cart.Run(cart.Cmd(), []string{"testorder"})) + tests.Compare(t, b.Out.String(), "testorder successfully deleted.\n") + cart.delete = false + b.Out.Reset() + cart.Cmd().ParseFlags([]string{}) + tests.Check(cart.Run(cart.Cmd(), []string{})) + tests.Compare(t, b.Out.String(), "No orders saved.\n") + b.Out.Reset() + tests.Exp(cart.Run(cart.Cmd(), []string{"not_a_real_order"})) + cart.topping = false + cart.validate = true + tests.Check(cart.Run(cart.Cmd(), []string{})) +} + +// func testAddToppings(cart *cartCmd, buf *bytes.Buffer, t *testing.T) { +func TestAddToppings(t *testing.T) { + b := cmdtest.NewTestRecorder(t) + defer b.CleanUp() + cart := newTestCart(b) + + cart.add = []string{"10SCREEN"} + tests.Check(cart.Run(cart.Cmd(), []string{"testorder"})) + cart.add = nil + + cart.product = "10SCREEN" + cart.add = []string{"P", "K"} + cart.topping = false + tests.Check(cart.Run(cart.Cmd(), []string{"testorder"})) + + cart.product = "" + cart.add = []string{} + cart.topping = false + b.Out.Reset() + tests.Check(cart.Run(cart.Cmd(), []string{"testorder"})) + + expected := `Small (10") Hand Tossed Pizza + code: 10SCREEN + options: + C: 1/1 1 + K: 1/1 1.0 + P: 1/1 1.0 + X: 1/1 1 + quantity: 1` + + if !strings.Contains(b.Out.String(), expected) { + t.Error("bad output") + } + b.Out.Reset() + + cart.topping = false + cart.product = "10SCREEN" + cart.remove = "C" + tests.Check(cart.Run(cart.Cmd(), []string{"testorder"})) + b.Out.Reset() + cart.topping = false + cart.product = "" + cart.remove = "" + tests.Check(cart.Run(cart.Cmd(), []string{"testorder"})) + /* + expected = ` Small (10") Hand Tossed Pizza + code: 10SCREEN + options: + C: 1/1 1 + K: 1/1 1.0 + P: 1/1 1.0 + X: 1/1 1 + quantity: 1` + if !strings.Contains(b.Out.String(), expected) { + fmt.Println("got:") + fmt.Println(b.Out.String()) + fmt.Println("expected:") + fmt.Print(expected) + t.Error("bad output") + } + */ + b.Out.Reset() + + cart.topping = false + cart.remove = "10SCREEN" + tests.Check(cart.Run(cart.Cmd(), []string{"testorder"})) + if strings.Contains(b.Out.String(), expected) { + t.Error("bad output") + } +} + +func TestOrder(t *testing.T) { + r := cmdtest.NewTestRecorder(t) + defer r.CleanUp() + cmd := NewOrderCmd(r).(*orderCmd) + addTestOrder(r) + + r.Conf.Card.Number = "38790546741937" + r.Conf.Card.Expiration = "01/01" + tests.Check(cmd.Cmd().ParseFlags([]string{"--log-only", "--cvv=123"})) + tests.Check(cmd.Run(cmd.Cmd(), []string{"testorder"})) + r.Conf.Address = obj.Address{ + Street: "600 Mountain Ave bldg 5", + CityName: "New Providence", + State: "NJ", + Zipcode: "07974", + } + tests.Check(cmd.Cmd().ParseFlags([]string{"--log-only", "--cvv=123"})) + tests.Check(cmd.Run(cmd.Cmd(), []string{"testorder"})) + tests.Check(cmd.Cmd().ParseFlags([]string{})) + tests.Check(cmd.Cmd().PreRunE(cmd.Cmd(), []string{})) + tests.Exp(cmd.Cmd().PreRunE(cmd.Cmd(), []string{"one", "two"})) +} + +func TestOrder_Err(t *testing.T) { + r := cmdtest.NewTestRecorder(t) + defer r.CleanUp() + addTestOrder(r) + + ordercmd := NewOrderCmd(r) + err := ordercmd.Run(ordercmd.Cmd(), []string{}) + tests.Check(err) + tests.Exp(ordercmd.Run(ordercmd.Cmd(), []string{"one", "two"})) + tests.Exp(ordercmd.Run(ordercmd.Cmd(), []string{"anorder"})) + cmd := ordercmd.(*orderCmd) + cmd.cvv = 100 + tests.Exp(cmd.Run(cmd.Cmd(), []string{"nothere"})) + cmd.cvv = 0 + + cmd.Cmd().ParseFlags([]string{"--log-only"}) + tests.Exp(cmd.Run(cmd.Cmd(), []string{"testorder"})) + cmd.Cmd().ParseFlags([]string{"--log-only", "--cvv=123"}) + tests.Exp(cmd.Run(cmd.Cmd(), []string{"testorder"})) + cmd.Cmd().ParseFlags([]string{"--log-only", "--cvv=123", "--number=38790546741937"}) + tests.Exp(cmd.Run(cmd.Cmd(), []string{"testorder"})) +} + +func TestEitherOr(t *testing.T) { + if eitherOr("one", "") != "one" { + t.Error("wrong result from 'eitherOr'") + } + if eitherOr("", "two") != "two" { + t.Error("wrong result from 'eitherOr'") + } + if eitherOr("a", "b") != "a" { + t.Error("wrong result from 'eitherOr'") + } +} diff --git a/cmd/commands/command.go b/cmd/commands/command.go new file mode 100644 index 0000000..a065f19 --- /dev/null +++ b/cmd/commands/command.go @@ -0,0 +1,55 @@ +package commands + +import ( + "errors" + "fmt" + "strings" + + "github.com/harrybrwn/apizza/cmd/cli" + "github.com/spf13/cobra" +) + +// Color toggles output color +// +// TODO: this is shitty code FIXME!!! +var Color = true + +// NewCompletionCmd creates a new command for shell completion. +func NewCompletionCmd(b cli.Builder) *cobra.Command { + var validArgs = []string{"zsh", "bash", "ps", "powershell", "fish"} + + cmd := &cobra.Command{ + Use: "completion [bash|zsh|powershell]", + Short: "Generate bash, zsh, or powershell completion", + Long: `Generate bash, zsh, or powershell completion +just add '. <(apizza completion )' to you .bashrc or .zshrc +note: for zsh you will need to run 'compdef _apizza apizza'`, + SilenceErrors: true, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) (err error) { + root := cmd.Root() + out := cmd.OutOrStdout() + + if len(args) == 0 { + return fmt.Errorf( + "no shell type given; (expected %s, or %s)", + strings.Join(validArgs[:len(validArgs)-1], ", "), + validArgs[len(validArgs)-1]) + } + switch args[0] { + case "zsh": + return root.GenZshCompletion(out) + case "ps", "powershell": + return root.GenPowerShellCompletion(out) + case "bash": + return root.GenBashCompletion(out) + case "fish": + return root.GenFishCompletion(out, false) + } + return errors.New("unknown shell type") + }, + ValidArgs: validArgs, + Aliases: []string{"comp"}, + } + return cmd +} diff --git a/cmd/command/config.go b/cmd/commands/config.go similarity index 75% rename from cmd/command/config.go rename to cmd/commands/config.go index d83dc47..4f60058 100644 --- a/cmd/command/config.go +++ b/cmd/commands/config.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package command +package commands import ( "errors" @@ -23,20 +23,19 @@ import ( "github.com/spf13/cobra" "github.com/harrybrwn/apizza/cmd/cli" + "github.com/harrybrwn/apizza/pkg/cache" "github.com/harrybrwn/apizza/pkg/config" ) -// var cfg = &base.Config{} - type configCmd struct { cli.CliCommand - file bool - dir bool - getall bool - edit bool + cli.AddressBuilder + db *cache.DataBase + conf *cli.Config - card string - exp string + file bool + dir bool + edit bool } func (c *configCmd) Run(cmd *cobra.Command, args []string) error { @@ -51,32 +50,30 @@ func (c *configCmd) Run(cmd *cobra.Command, args []string) error { c.Println(config.Folder()) return nil } - if c.getall { - return config.FprintAll(cmd.OutOrStdout(), config.Object()) - } - return cmd.Usage() + return config.FprintAll(cmd.OutOrStdout(), config.Object()) } // NewConfigCmd creates a new config command. func NewConfigCmd(b cli.Builder) cli.CliCommand { - c := &configCmd{file: false, dir: false} + c := &configCmd{ + AddressBuilder: b, + db: b.DB(), + conf: b.Config(), + file: false, + dir: false, + } c.CliCommand = b.Build("config", "Configure apizza", c) c.SetOutput(b.Output()) - c.Cmd().Long = `The 'config' command is used for accessing the .apizza config file -in your home directory. Feel free to edit the .apizza json file -by hand or use the 'config' command. - -ex. 'apizza config get name' or 'apizza config set name='` + cmd := c.Cmd() + cmd.Aliases = []string{"conf"} + cmd.Long = `The 'config' command is used for accessing the apizza config file +in your home directory. Feel free to edit the apizza config.json file +by hand or use the 'config' command.` c.Flags().BoolVarP(&c.file, "file", "f", c.file, "show the path to the config.json file") c.Flags().BoolVarP(&c.dir, "dir", "d", c.dir, "show the apizza config directory path") - c.Flags().BoolVar(&c.getall, "get-all", c.getall, "show all the contents of the config file") - c.Flags().BoolVarP(&c.edit, "edit", "e", false, "open the conifg file with the text editor set by $EDITOR") - - c.Flags().StringVar(&c.card, "card", "", "store encrypted credit card number in the database") - c.Flags().StringVar(&c.exp, "expiration", "", "store the encrypted expiration data of your credit card") + c.Flags().BoolVarP(&c.edit, "edit", "e", false, "open the config file with the text editor set by $EDITOR") - cmd := c.Cmd() cmd.AddCommand(configSetCmd, configGetCmd) return c } diff --git a/cmd/command/config_test.go b/cmd/commands/config_test.go similarity index 55% rename from cmd/command/config_test.go rename to cmd/commands/config_test.go index cc6cf97..4901974 100644 --- a/cmd/command/config_test.go +++ b/cmd/commands/config_test.go @@ -1,9 +1,10 @@ -package command +package commands import ( "bytes" "encoding/json" "os" + "strings" "testing" "github.com/harrybrwn/apizza/cmd/cli" @@ -22,6 +23,7 @@ var testconfigjson = ` "cityName":"Washington DC", "state":"","zipcode":"20500" }, + "default-address-name": "", "card":{"number":"","expiration":"","cvv":""}, "service":"Carryout" }` @@ -34,6 +36,7 @@ address: cityname: "Washington DC" state: "" zipcode: "20500" +default-address-name: "" card: number: "" expiration: "" @@ -41,91 +44,49 @@ service: "Carryout" ` func TestConfigStruct(t *testing.T) { + tests.InitHelpers(t) r := cmdtest.NewRecorder() r.ConfigSetup([]byte(testconfigjson)) - defer func() { - r.CleanUp() - // config.SetNonFileConfig(cfg) // for test compatability - }() - // check(json.Unmarshal([]byte(testconfigjson), r.Config()), "json") - if err := json.Unmarshal([]byte(testconfigjson), r.Config()); err != nil { - t.Fatal(err) - } + defer func() { r.CleanUp() }() + tests.Fatal(json.Unmarshal([]byte(testconfigjson), r.Config())) - if r.Config().Get("name").(string) != "joe" { - t.Error("wrong value") - } - if err := r.Config().Set("name", "not joe"); err != nil { - t.Error(err) - } - if r.Config().Get("Name").(string) != "not joe" { - t.Error("wrong value") - } - if err := r.Config().Set("name", "joe"); err != nil { - t.Error(err) - } + tests.StrEq(r.Config().Get("name").(string), "joe", "wrong value from Config.Get") + tests.Check(r.Config().Set("name", "not joe")) + tests.StrEq(r.Config().Get("Name").(string), "not joe", "wrong value from Config.Get") + tests.Check(r.Config().Set("name", "joe")) } func TestConfigCmd(t *testing.T) { + tests.InitHelpers(t) r := cmdtest.NewRecorder() c := NewConfigCmd(r).(*configCmd) r.ConfigSetup([]byte(testconfigjson)) - defer func() { - r.CleanUp() - // config.SetNonFileConfig(cfg) // for test compatability - }() + defer r.CleanUp() c.file = true - if err := c.Run(c.Cmd(), []string{}); err != nil { - t.Error(err) - } + tests.Check(c.Run(c.Cmd(), []string{})) c.file = false r.Compare(t, "\n") r.ClearBuf() c.dir = true - if err := c.Run(c.Cmd(), []string{}); err != nil { - t.Error(err) - } + tests.Check(c.Run(c.Cmd(), []string{})) r.Compare(t, "\n") r.ClearBuf() - err := json.Unmarshal([]byte(testconfigjson), r.Config()) - if err != nil { - t.Error(err) - } + tests.Check(json.Unmarshal([]byte(testconfigjson), r.Config())) c.dir = false - c.getall = true - if err := c.Run(c.Cmd(), []string{}); err != nil { - t.Error(err) - } + tests.Check(c.Run(c.Cmd(), []string{})) r.Compare(t, testConfigOutput) r.ClearBuf() - c.getall = false - cmdUseage := c.Cmd().UsageString() - if err := c.Run(c.Cmd(), []string{}); err != nil { - t.Error(err) - } - r.Compare(t, cmdUseage) - r.ClearBuf() - if err := c.Run(c.Cmd(), []string{}); err != nil { - t.Error(err) - } - r.Compare(t, c.Cmd().UsageString()) } func TestConfigEdit(t *testing.T) { + tests.InitHelpers(t) r := cmdtest.NewRecorder() c := NewConfigCmd(r).(*configCmd) - err := config.SetConfig(".apizza/tests", r.Conf) - if err != nil { - t.Error(err) - } + tests.Check(config.SetConfig(".config/apizza/tests", r.Conf)) defer func() { - err = errs.Pair(r.DB().Destroy(), os.RemoveAll(config.Folder())) - if err != nil { - t.Error() - } - // config.SetNonFileConfig(cfg) // for test compatability + tests.Check(errs.Pair(r.DB().Destroy(), os.RemoveAll(config.Folder()))) }() os.Setenv("EDITOR", "cat") @@ -140,29 +101,28 @@ func TestConfigEdit(t *testing.T) { "State": "", "Zipcode": "" }, + "DefaultAddressName": "", "Card": { "Number": "", "Expiration": "" }, "Service": "Delivery" }` + if exp == "" { + t.Error("no this should not happed") + } t.Run("edit output", func(t *testing.T) { - if os.Getenv("TRAVIS") == "true" { + if os.Getenv("TRAVIS") != "true" { // for some reason, 'cat' in travis gives no output - t.Skip() + // tests.CompareOutput(t, exp, func() { + // tests.Check(c.Run(c.Cmd(), []string{})) + // fmt.Println(exp) + // }) } - tests.CompareOutput(t, exp, func() { - if err = c.Run(c.Cmd(), []string{}); err != nil { - t.Error(err) - } - }) }) c.edit = false - err = json.Unmarshal([]byte(testconfigjson), r.Config()) - if err != nil { - t.Error(err) - } + tests.Check(json.Unmarshal([]byte(testconfigjson), r.Config())) a := config.Get("address") if a == nil { t.Error("should not be nil") @@ -177,27 +137,20 @@ func TestConfigEdit(t *testing.T) { } func TestConfigGet(t *testing.T) { + tests.InitHelpers(t) conf := &cli.Config{} config.SetNonFileConfig(conf) // don't want it to over ride the file on disk - if err := json.Unmarshal([]byte(testconfigjson), conf); err != nil { - t.Fatal(err) - } - + tests.Fatal(json.Unmarshal([]byte(testconfigjson), conf)) buf := &bytes.Buffer{} args := []string{"email", "name"} - err := get(args, buf) - if err != nil { - t.Error(err) - } - if err := configGetCmd.RunE(configGetCmd, []string{"email", "name"}); err != nil { - t.Error(err) - } + tests.Check(get(args, buf)) + tests.Check(configGetCmd.RunE(configGetCmd, []string{"email", "name"})) tests.Compare(t, buf.String(), "nojoe@mail.com\njoe\n") buf.Reset() args = []string{} - err = get(args, buf) + err := get(args, buf) if err == nil { t.Error("expected error") } else if err.Error() != "no variable given" { @@ -225,19 +178,13 @@ func TestConfigGet(t *testing.T) { } func TestConfigSet(t *testing.T) { - // c := newConfigSet() //.(*configSetCmd) + tests.InitHelpers(t) conf := &cli.Config{} config.SetNonFileConfig(conf) // don't want it to over ride the file on disk - if err := json.Unmarshal([]byte(cmdtest.TestConfigjson), conf); err != nil { - t.Fatal(err) - } + tests.Fatal(json.Unmarshal([]byte(cmdtest.TestConfigjson), conf)) - if err := configSetCmd.RunE(configSetCmd, []string{"name=someNameOtherThanJoe"}); err != nil { - t.Error(err) - } - if config.GetString("name") != "someNameOtherThanJoe" { - t.Error("did not set the name correctly") - } + tests.Check(configSetCmd.RunE(configSetCmd, []string{"name=someNameOtherThanJoe"})) + tests.StrEq(config.GetString("name"), "someNameOtherThanJoe", "did not set the name correctly") if err := configSetCmd.RunE(configSetCmd, []string{}); err == nil { t.Error("expected error") } else if err.Error() != "no variable given" { @@ -252,3 +199,60 @@ func TestConfigSet(t *testing.T) { t.Error("wrong error message, got:", err.Error()) } } + +func TestCompletion(t *testing.T) { + r := cmdtest.NewTestRecorder(t) + defer r.CleanUp() + c := NewCompletionCmd(r) + c.SetOutput(r.Out) + for _, a := range []string{"zsh", "ps", "powershell", "bash", "fish"} { + if err := c.RunE(c, []string{a}); err != nil { + if r.Out.Len() == 0 { + t.Error("got zero length completion script") + } + } + } +} + +func TestAddressCmd(t *testing.T) { + r := cmdtest.NewTestRecorder(t) + defer r.CleanUp() + buf := &bytes.Buffer{} + cmd := NewAddAddressCmd(r, buf).(*addAddressCmd) + tests.Check(cmd.Cmd().ParseFlags([]string{"--new"})) + + inputs := []string{ + "testaddress", + "600 Mountain Ave bldg 5", + "New Providence", + "NJ", + "07974", + } + for _, in := range inputs { + _, err := buf.Write([]byte(in + "\n")) + tests.Check(err) + } + tests.Check(cmd.Run(cmd.Cmd(), []string{})) + raw, err := r.DataBase.WithBucket("addresses").Get("testaddress") + tests.Check(err) + addr, err := obj.FromGob(raw) + tests.Check(err) + tests.StrEq(addr.Street, "600 Mountain Ave bldg 5", "got wrong street") + tests.StrEq(addr.CityName, "New Providence", "got wrong city") + tests.StrEq(addr.State, "NJ", "go wrong state") + tests.StrEq(addr.Zipcode, "07974", "got wrong zip") + + r.Out.Reset() + cmd.new = false + tests.Check(cmd.Run(cmd.Cmd(), []string{})) + if !strings.Contains(r.Out.String(), obj.AddressFmtIndent(addr, 2)) { + t.Error("address was no found in output") + } + r.Out.Reset() + + tests.Check(cmd.Cmd().ParseFlags([]string{"--delete=testaddress"})) + tests.Check(cmd.Run(cmd.Cmd(), []string{})) + if r.Out.Len() != 0 { + t.Error("should be zero length") + } +} diff --git a/cmd/internal/cmdtest/cleanup_go1.14.go b/cmd/internal/cmdtest/cleanup_go1.14.go new file mode 100644 index 0000000..c826785 --- /dev/null +++ b/cmd/internal/cmdtest/cleanup_go1.14.go @@ -0,0 +1,15 @@ +// +build go1.14 + +package cmdtest + +import "github.com/harrybrwn/apizza/pkg/tests" + +// CleanUp is a noop for go1.14 +func (tr *TestRecorder) CleanUp() {} + +func (tr *TestRecorder) init() { + tr.t.Cleanup(func() { + tr.Recorder.CleanUp() + tests.ResetHelpers() + }) +} diff --git a/cmd/internal/cmdtest/cleanup_notgo1.14.go b/cmd/internal/cmdtest/cleanup_notgo1.14.go new file mode 100644 index 0000000..32a82d5 --- /dev/null +++ b/cmd/internal/cmdtest/cleanup_notgo1.14.go @@ -0,0 +1,14 @@ +// +build !go1.14 + +package cmdtest + +import "github.com/harrybrwn/apizza/pkg/tests" + +// CleanUp cleans up all the TestRecorder's allocated recourses +func (tr *TestRecorder) CleanUp() { + tr.Recorder.CleanUp() + tests.ResetHelpers() +} + +// init is a noop for builds below 1.14 +func (tr *TestRecorder) init() {} diff --git a/cmd/internal/cmdtest/recorder.go b/cmd/internal/cmdtest/recorder.go index 188f8ba..008c8ff 100644 --- a/cmd/internal/cmdtest/recorder.go +++ b/cmd/internal/cmdtest/recorder.go @@ -3,11 +3,16 @@ package cmdtest import ( "bytes" "encoding/json" + "fmt" "io" + "io/ioutil" + "log" + "os" "strings" "testing" "github.com/harrybrwn/apizza/cmd/cli" + "github.com/harrybrwn/apizza/cmd/opts" "github.com/harrybrwn/apizza/dawg" "github.com/harrybrwn/apizza/pkg/cache" "github.com/harrybrwn/apizza/pkg/config" @@ -24,25 +29,31 @@ type Recorder struct { addr dawg.Address } -// TODO: -// - give the inner config an actual temp file and delete it in -// the CleanUp function. (need to get rid of global cfg var first) - var services = []string{dawg.Carryout, dawg.Delivery} // NewRecorder create a new command recorder. func NewRecorder() *Recorder { addr := TestAddress() + conf := &cli.Config{} + config.DefaultOutput = ioutil.Discard + err := config.SetConfig(".config/apizza/.tests", conf) + if err != nil { + panic(err.Error()) + } + + conf.Name = "Apizza TestRecorder" + conf.Service = dawg.Carryout + conf.Address = *addr + + out := new(bytes.Buffer) + log.SetOutput(ioutil.Discard) + return &Recorder{ - DataBase: TempDB(), - Out: new(bytes.Buffer), - Conf: &cli.Config{ - Name: "Apizza TestRecorder", - Service: dawg.Carryout, - Address: *addr, - }, - addr: addr, - cfgHasFile: false, + DataBase: TempDB(), + Out: out, + Conf: conf, + addr: nil, + cfgHasFile: true, } } @@ -70,7 +81,15 @@ func (r *Recorder) Build(use, short string, run cli.Runner) *cli.Command { // Address returns the address. func (r *Recorder) Address() dawg.Address { - return r.addr + if r.addr != nil { + return r.addr + } + return &r.Conf.Address +} + +// GlobalOptions has the global flags +func (r *Recorder) GlobalOptions() *opts.CliFlags { + return &opts.CliFlags{} } // ToApp returns the arguments needed to create a cmd.App. @@ -80,8 +99,21 @@ func (r *Recorder) ToApp() (*cache.DataBase, *cli.Config, io.Writer) { // CleanUp will cleanup all the the Recorder tempfiles and free all resources. func (r *Recorder) CleanUp() { - if err := r.DataBase.Destroy(); err != nil { - panic(err) + var err error + if r.cfgHasFile && config.File() != "" && config.Folder() != "" { + err = config.Save() + if err = config.Save(); err != nil { + // panic(err) + fmt.Println("Error:", err) + } + if err = os.Remove(config.File()); err != nil { + // panic(err) + fmt.Println("Error:", err) + } + } + if err = r.DataBase.Destroy(); err != nil { + // panic(err) + fmt.Println("Error:", err) } } @@ -138,3 +170,22 @@ func (r *Recorder) StrEq(s string) bool { func (r *Recorder) Compare(t *testing.T, expected string) { tests.CompareCallDepth(t, r.Out.String(), expected, 2) } + +// TestRecorder is a Recorder that has access to a testing.T +type TestRecorder struct { + *Recorder + t *testing.T +} + +// NewTestRecorder creates a new TestRecorder +func NewTestRecorder(t *testing.T) *TestRecorder { + tests.InitHelpers(t) + tr := &TestRecorder{ + Recorder: NewRecorder(), + t: t, + } + tr.init() + return tr +} + +var _ cli.Builder = (*TestRecorder)(nil) diff --git a/cmd/internal/cmdtest/util.go b/cmd/internal/cmdtest/util.go index 0991af7..665243d 100644 --- a/cmd/internal/cmdtest/util.go +++ b/cmd/internal/cmdtest/util.go @@ -2,6 +2,7 @@ package cmdtest import ( "github.com/harrybrwn/apizza/cmd/internal/obj" + "github.com/harrybrwn/apizza/dawg" "github.com/harrybrwn/apizza/pkg/cache" "github.com/harrybrwn/apizza/pkg/tests" ) @@ -25,6 +26,25 @@ func TempDB() *cache.DataBase { return db } +// OrderName is the name of all testing orders created by the cmdtest package. +const OrderName = "cmdtest.TestingOrder" + +// NewTestOrder creates an order for testing. +func NewTestOrder() *dawg.Order { + o := &dawg.Order{ + StoreID: "4336", + Address: dawg.StreetAddrFromAddress(TestAddress()), + FirstName: "Jimmy", + LastName: "James", + OrderName: OrderName, + LanguageCode: dawg.DefaultLang, + ServiceMethod: dawg.Delivery, + Products: []*dawg.OrderProduct{}, + } + o.Init() + return o +} + // TestConfigjson data. var TestConfigjson = ` { diff --git a/cmd/internal/data/managedb.go b/cmd/internal/data/managedb.go index 41fe8cf..49b4055 100644 --- a/cmd/internal/data/managedb.go +++ b/cmd/internal/data/managedb.go @@ -14,17 +14,34 @@ import ( "github.com/harrybrwn/apizza/pkg/errs" ) -// OrderPrefix is the prefix added to user orders when stored in a database. -const OrderPrefix = "user_order_" +const ( + // OrderPrefix is the prefix added to user orders when stored in a database. + OrderPrefix = "user_order_" -// NewDatabase make the default database. -func NewDatabase() (*cache.DataBase, error) { - dbPath := filepath.Join(config.Folder(), "cache", "apizza.db") + // DataBaseName is the filename for the program's local storage. + DataBaseName = "apizza.db" +) + +// OpenDatabase make the default database. +func OpenDatabase() (*cache.DataBase, error) { + dbPath := filepath.Join(config.Folder(), "cache", DataBaseName) return cache.GetDB(dbPath) } +// ListOrders will return a list of orders stored in the database. +func ListOrders(db cache.MapDB) []string { + all, _ := db.Map() + names := make([]string, 0, len(all)) // its going to be at least as big + for key := range all { + if strings.Contains(key, OrderPrefix) { + names = append(names, strings.Replace(key, OrderPrefix, "", -1)) + } + } + return names +} + // PrintOrders will print all the names of the saved user orders -func PrintOrders(db cache.MapDB, w io.Writer, verbose bool) error { +func PrintOrders(db cache.MapDB, w io.Writer, verbose bool, color string) error { all, err := db.Map() if err != nil { return err @@ -32,7 +49,7 @@ func PrintOrders(db cache.MapDB, w io.Writer, verbose bool) error { out.SetOutput(w) var ( - orders []string + orders = make([]string, 0, len(all)) // at least as big as all uOrders []*dawg.Order tempOrder *dawg.Order ) @@ -57,10 +74,17 @@ func PrintOrders(db cache.MapDB, w io.Writer, verbose bool) error { return nil } - fmt.Fprintln(w, "Your Orders:") + var yesColor bool + var endcolor = "" + if color != "" { + yesColor = true + endcolor = "\033[0m" + + } + fmt.Fprintf(w, "%sYour Orders%s:\n", color, endcolor) for i, o := range orders { if verbose { - err = out.PrintOrder(uOrders[i], false, false) + err = out.PrintOrder(uOrders[i], false, yesColor, false) if err != nil { return err } @@ -98,7 +122,6 @@ func SaveOrder(o *dawg.Order, w io.Writer, db cache.Putter) error { } else { return err } - err = dawg.ValidateOrder(o) if dawg.IsFailure(err) { return err diff --git a/cmd/internal/data/managedb_test.go b/cmd/internal/data/managedb_test.go index cc824f4..a886904 100644 --- a/cmd/internal/data/managedb_test.go +++ b/cmd/internal/data/managedb_test.go @@ -24,84 +24,60 @@ func init() { testStore, _ = dawg.NearestStore(a, "Delivery") } -func TestDBManagment(t *testing.T) { +func TestDBManagement(t *testing.T) { + tests.InitHelpers(t) db := cmdtest.TempDB() - defer db.Destroy() - var err error o := testStore.NewOrder() o.SetName("test_order") buf := &bytes.Buffer{} - if err = PrintOrders(db, buf, false); err != nil { - t.Error(err) - } + tests.Check(PrintOrders(db, buf, false, "")) tests.Compare(t, buf.String(), "No orders saved.\n") buf.Reset() - if err = SaveOrder(o, buf, db); err != nil { - t.Error(err) - } + tests.Check(SaveOrder(o, buf, db)) tests.Compare(t, buf.String(), "order successfully updated.\n") buf.Reset() - if err = PrintOrders(db, buf, false); err != nil { - t.Error(err) - } + tests.Check(PrintOrders(db, buf, false, "")) tests.Compare(t, buf.String(), "Your Orders:\n test_order\n") buf.Reset() - if _, err := GetOrder("badorder", db); err == nil { - t.Error("expected error") - } + _, err = GetOrder("badorder", db) + tests.Exp(err) newO, err := GetOrder("test_order", db) - if err != nil { - t.Error(err) - } - if newO.Name() != o.Name() { - t.Error("wrong order") - } - if newO.Address.LineOne() != o.Address.LineOne() { - t.Error("wrong address saved") - } - if newO.Address.City() != o.Address.City() { - t.Error("wrong address saved") - } - if err = db.Destroy(); err != nil { - t.Error(err) - } + tests.Check(err) + tests.StrEq(newO.Name(), o.Name(), "wrong order") + tests.StrEq(newO.Address.LineOne(), o.Address.LineOne(), "wrong address saved") + tests.StrEq(newO.Address.City(), o.Address.City(), "wrong address saved") + tests.Check(db.Destroy()) } func TestPrintOrders(t *testing.T) { + tests.InitHelpers(t) var err error o := testStore.NewOrder() p, err := testStore.GetVariant("10SCREEN") - if err != nil { - t.Error(err) - } + tests.Check(err) if p == nil { t.Fatal("got nil product") } - if err = o.AddProductQty(p, 10); err != nil { - t.Error(err) - } + tests.Check(o.AddProductQty(p, 10)) db := cmdtest.TempDB() - defer db.Destroy() + defer func() { tests.Check(db.Destroy()) }() o.SetName("test_order") buf := new(bytes.Buffer) - if err = SaveOrder(o, buf, db); err != nil { - t.Error(err) - } + tests.Check(SaveOrder(o, buf, db)) buf.Reset() - if err = PrintOrders(db, buf, true); err != nil { - t.Error(err) - } - exp := "Your Orders:\n test_order - 10SCREEN, \n" - tests.Compare(t, buf.String(), exp) + tests.Check(PrintOrders(db, buf, true, "")) + tests.Compare(t, buf.String(), "Your Orders:\n test_order - 10SCREEN, \n") } -func TestMenuCacher(t *testing.T) { +func TestMenuCacherJSON(t *testing.T) { + t.Skip() + tests.InitHelpers(t) var err error db := cmdtest.TempDB() defer db.Destroy() @@ -111,7 +87,7 @@ func TestMenuCacher(t *testing.T) { log.SetFlags(0) log.SetOutput(&buf) - c := cacher.(*menuCache) + c := cacher.(*generalMenuCacher) if c.m != nil { t.Error("cacher should not have a menu yet") } @@ -119,9 +95,7 @@ func TestMenuCacher(t *testing.T) { t.Error("cacher should not have a menu yet") } - if err = db.UpdateTS("menu", cacher); err != nil { - t.Error(err) - } + tests.Check(db.UpdateTS("menu", cacher)) if c.m == nil { t.Error("cacher should have a menu now") } @@ -129,9 +103,7 @@ func TestMenuCacher(t *testing.T) { t.Error("cacher should have a menu now") } data, err := db.Get("menu") - if err != nil { - t.Error(err) - } + tests.Check(err) if len(data) == 0 { t.Error("should have stored a menu") } @@ -141,11 +113,7 @@ func TestMenuCacher(t *testing.T) { } buf.Reset() - if err = db.UpdateTS("menu", cacher); err != nil { - t.Error(err) - } + tests.Check(db.UpdateTS("menu", cacher)) c.m = nil - if err = db.UpdateTS("menu", c); err != nil { - t.Error(err) - } + tests.Check(db.UpdateTS("menu", c)) } diff --git a/cmd/internal/data/menu_cache.go b/cmd/internal/data/menu_cache.go index 9abee82..bf74162 100644 --- a/cmd/internal/data/menu_cache.go +++ b/cmd/internal/data/menu_cache.go @@ -1,7 +1,10 @@ package data import ( + "bytes" + "encoding/gob" "encoding/json" + "io" "log" "time" @@ -23,49 +26,103 @@ func NewMenuCacher( db cache.Storage, store func() *dawg.Store, ) MenuCacher { - mc := &menuCache{ - m: nil, - db: db, - getstore: store, - } - mc.Updater = cache.NewUpdater(decay, mc.cacheNewMenu, mc.getCachedMenu) - return mc + // use gob to cache the menu in binary format + return NewGobMenuCacher(decay, db, store) +} + +func init() { + gob.Register([]interface{}{}) +} + +// Encoder is an interface that defines objects that +// are able to Encode and interface. +type Encoder interface { + Encode(interface{}) error +} + +// Decoder is an interface that defines objects +// that can decode and interface. +type Decoder interface { + Decode(interface{}) error } -type menuCache struct { +type generalMenuCacher struct { cache.Updater m *dawg.Menu db cache.Storage getstore func() *dawg.Store + + newEncoder func(io.Writer) Encoder + newDecoder func(io.Reader) Decoder } -func (mc *menuCache) Menu() *dawg.Menu { +// NewJSONMenuCacher will create a new MenuCacher that stores the +// menu as json. +func NewJSONMenuCacher( + decay time.Duration, + db cache.Storage, + store func() *dawg.Store, +) MenuCacher { + mc := &generalMenuCacher{ + m: nil, + db: db, + getstore: store, + newEncoder: func(w io.Writer) Encoder { return json.NewEncoder(w) }, + newDecoder: func(r io.Reader) Decoder { return json.NewDecoder(r) }, + } + mc.Updater = cache.NewUpdater(decay, mc.cacheNewMenu, mc.getCachedMenu) + return mc +} + +// NewGobMenuCacher will create a MenuCacher that will store the menu +// in a binary format using the "encoding/gob" package. +func NewGobMenuCacher( + decay time.Duration, + db cache.Storage, + store func() *dawg.Store, +) MenuCacher { + mc := &generalMenuCacher{ + m: nil, + db: db, + getstore: store, + newEncoder: func(w io.Writer) Encoder { return gob.NewEncoder(w) }, + newDecoder: func(r io.Reader) Decoder { return gob.NewDecoder(r) }, + } + mc.Updater = cache.NewUpdater(decay, mc.cacheNewMenu, mc.getCachedMenu) + return mc +} + +func (mc *generalMenuCacher) Menu() *dawg.Menu { if mc.m != nil { return mc.m } return nil } -func (mc *menuCache) cacheNewMenu() error { +func (mc *generalMenuCacher) cacheNewMenu() error { var e1, e2 error - var raw []byte mc.m, e1 = mc.getstore().Menu() log.Println("caching another menu") - raw, e2 = json.Marshal(mc.m) - return errs.Append(e1, e2, mc.db.Put("menu", raw)) + + buf := &bytes.Buffer{} + e2 = mc.newEncoder(buf).Encode(mc.m) + return errs.Append(e1, e2, mc.db.Put("menu", buf.Bytes())) } -func (mc *menuCache) getCachedMenu() error { +func (mc *generalMenuCacher) getCachedMenu() error { if mc.m == nil { mc.m = new(dawg.Menu) raw, err := mc.db.Get("menu") if raw == nil { return mc.cacheNewMenu() } - err = errs.Pair(err, json.Unmarshal(raw, mc.m)) + + dec := mc.newDecoder(bytes.NewBuffer(raw)) + err = errs.Pair(err, dec.Decode(mc.m)) if err != nil { return err } + if mc.m.ID != mc.getstore().ID { return mc.cacheNewMenu() } @@ -73,4 +130,4 @@ func (mc *menuCache) getCachedMenu() error { return nil } -var _ MenuCacher = (*menuCache)(nil) +var _ MenuCacher = (*generalMenuCacher)(nil) diff --git a/cmd/internal/erros.go b/cmd/internal/erros.go new file mode 100644 index 0000000..f30cad2 --- /dev/null +++ b/cmd/internal/erros.go @@ -0,0 +1,12 @@ +package internal + +import "errors" + +var ( + // ErrNoAddress is the error found when the cli could no find an address + ErrNoAddress = errors.New("no address found. (see 'apizza address' or 'apizza config')") + + // ErrNoOrderName is the error raised when the is no order name given to the + // cart or the order commands. + ErrNoOrderName = errors.New("No order name... use '--name=' or give name as an argument") +) diff --git a/cmd/internal/obj/address.go b/cmd/internal/obj/address.go index e361302..4580050 100644 --- a/cmd/internal/obj/address.go +++ b/cmd/internal/obj/address.go @@ -1,14 +1,15 @@ package obj import ( + "bytes" + "encoding/gob" + "encoding/json" "fmt" "strings" "github.com/harrybrwn/apizza/dawg" ) -var _ dawg.Address = (*Address)(nil) - // Address represents a street address type Address struct { Street string `config:"street" json:"street"` @@ -58,6 +59,8 @@ func (a *Address) Zip() string { return "" } +var _ dawg.Address = (*Address)(nil) + // AddressFmt returns a formatted address string from and Address interface. func AddressFmt(a dawg.Address) string { return AddressFmtIndent(a, 0) @@ -86,8 +89,29 @@ func (a Address) String() string { return AddressFmt(&a) } +// AsGob converts the Address into a binary format using the gob package. +func AsGob(a *Address) ([]byte, error) { + buf := &bytes.Buffer{} + err := gob.NewEncoder(buf).Encode(a) + return buf.Bytes(), err +} + +// FromGob will create a new address from binary data encoded using the gob package. +func FromGob(raw []byte) (*Address, error) { + a := &Address{} + return a, gob.NewDecoder(bytes.NewReader(raw)).Decode(a) +} + +// AsJSON converts the Address to json format. +func AsJSON(a *Address) ([]byte, error) { + return json.Marshal(a) +} + // AddrIsEmpty will tell if an address is empty. func AddrIsEmpty(a dawg.Address) bool { + if a == nil { + return true + } if a.LineOne() == "" && a.Zip() == "" && a.City() == "" && diff --git a/cmd/internal/obj/address_test.go b/cmd/internal/obj/address_test.go index 5a05b71..6ffff5f 100644 --- a/cmd/internal/obj/address_test.go +++ b/cmd/internal/obj/address_test.go @@ -4,9 +4,11 @@ import ( "testing" "github.com/harrybrwn/apizza/dawg" + "github.com/harrybrwn/apizza/pkg/tests" ) func TestAddressStr(t *testing.T) { + tests.InitHelpers(t) a := &Address{ Street: "1600 Pennsylvania Ave NW", CityName: "Washington", State: "dc", Zipcode: "20500", @@ -41,19 +43,10 @@ Washington DC, 20500`, } } addr := FromAddress(dawg.StreetAddrFromAddress(a)) - if addr.LineOne() != a.LineOne() { - t.Error("wrong lineone") - } - if addr.StateCode() != a.StateCode() { - t.Error("wrong state code") - } - if addr.City() != a.City() { - t.Error("wrong city") - } - if addr.Zip() != a.Zip() { - t.Error("wrong zip") - } - + tests.StrEq(addr.LineOne(), a.LineOne(), "wrong lineone") + tests.StrEq(addr.StateCode(), a.StateCode(), "wrong state code") + tests.StrEq(addr.City(), a.City(), "wrong city") + tests.StrEq(addr.Zip(), a.Zip(), "wrong zip") if AddrIsEmpty(addr) { t.Error("should not be empty") } diff --git a/cmd/internal/out/out.go b/cmd/internal/out/out.go index 433decf..557d56e 100644 --- a/cmd/internal/out/out.go +++ b/cmd/internal/out/out.go @@ -30,7 +30,7 @@ func ResetOutput() { output = _output } -// FormatLine will take a string and make sure it does not cross a certain lenth +// FormatLine will take a string and make sure it does not cross a certain length // by slicing it at a space closest to the length argument. func FormatLine(str string, length int) (lines []string) { strLen := utf8.RuneCountInString(str) @@ -85,7 +85,7 @@ func lineone(str string, start, length int) (string, int) { } // PrintOrder will print the order given. -func PrintOrder(o *dawg.Order, full, price bool) (err error) { +func PrintOrder(o *dawg.Order, full, color, price bool) (err error) { var ( t string oPrice float64 @@ -99,14 +99,24 @@ func PrintOrder(o *dawg.Order, full, price bool) (err error) { if price { oPrice, err = o.Price() } + + var keycolor, endcolor string + if color { + keycolor = "\033[01;34m" + endcolor = "\033[0m" + } data := struct { *dawg.Order - Addr string - Price float64 + Addr string + Price float64 + KeyColor string + EndColor string }{ - Order: o, - Addr: obj.AddressFmtIndent(o.Address, 11), - Price: oPrice, + Order: o, + Addr: obj.AddressFmtIndent(o.Address, 11), + Price: oPrice, + KeyColor: keycolor, + EndColor: endcolor, } return errs.Pair(err, tmpl(output, t, data)) } diff --git a/cmd/internal/out/out_test.go b/cmd/internal/out/out_test.go index fa1d124..8e3b1f1 100644 --- a/cmd/internal/out/out_test.go +++ b/cmd/internal/out/out_test.go @@ -12,18 +12,18 @@ import ( func TestFormatLine(t *testing.T) { exp := []string{ - "The menu command will show the dominos menu. To show a subdivition of the menu, ", + "The menu command will show the dominos menu. To show a subdivision of the menu, ", "give an item or category to the --category and --item flags or give them as an ", "argument to the command itself.", } - s := `The menu command will show the dominos menu. To show a subdivition of the menu, give an item or category to the --category and --item flags or give them as an argument to the command itself.` + s := `The menu command will show the dominos menu. To show a subdivision of the menu, give an item or category to the --category and --item flags or give them as an argument to the command itself.` for i, line := range FormatLine(s, 80) { if exp[i] != line { t.Error("wrong line format") } } - expected := "The menu command will show the dominos menu. To show a subdivition of the menu, \n give an item or category to the --category and --item flags or give them as an \n argument to the command itself." + expected := "The menu command will show the dominos menu. To show a subdivision of the menu, \n give an item or category to the --category and --item flags or give them as an \n argument to the command itself." tests.Compare(t, FormatLineIndent(s, 80, 4), expected) } @@ -50,28 +50,21 @@ func init() { } func TestPrintOrder(t *testing.T) { + tests.InitHelpers(t) o := testStore.MakeOrder("Jimbo", "Jones", "blahblah@aol.com") o.SetName("TestOrder") pizza, err := testStore.GetVariant("14SCREEN") - err = errs.Pair(err, o.AddProduct(pizza)) - if err != nil { - t.Error(err) - } + tests.Check(errs.Pair(err, o.AddProduct(pizza))) buf := new(bytes.Buffer) SetOutput(buf) - err = PrintOrder(o, false, false) - if err != nil { - t.Error(err) - } + tests.Check(PrintOrder(o, false, false, false)) tests.CompareV(t, buf.String(), " TestOrder - 14SCREEN, \n") buf.Reset() - if err = PrintOrder(o, true, false); err != nil { - t.Error(err) - } + tests.Check(PrintOrder(o, true, false, false)) expected := `TestOrder products: - Large (14") Hand Tossed Pizza + name: Large (14") Hand Tossed Pizza code: 14SCREEN options: C: full 1 @@ -84,31 +77,22 @@ func TestPrintOrder(t *testing.T) { ` tests.CompareV(t, buf.String(), expected) buf.Reset() - - if err = PrintOrder(o, true, true); err != nil { - t.Error(err) - } + tests.Check(PrintOrder(o, true, false, true)) tests.Compare(t, buf.String(), expected+" price: $20.15\n") ResetOutput() } func TestPrintItems(t *testing.T) { + tests.InitHelpers(t) menu, err := testStore.Menu() - if err != nil { - t.Error(err) - } + tests.Check(err) buf := new(bytes.Buffer) SetOutput(buf) defer ResetOutput() v, err := menu.GetVariant("14SCREEN") - if err != nil { - t.Error(err) - } - err = ItemInfo(v, menu) - if err != nil { - t.Error(err) - } + tests.Check(err) + tests.Check(ItemInfo(v, menu)) expected := `Large (14") Hand Tossed Pizza Code: 14SCREEN Category: Pizza @@ -119,7 +103,7 @@ func TestPrintItems(t *testing.T) { Parent Product: 'Pizza' [S_PIZZA] ` // we are not testing for the output of the toppings section - // because the order of the toppings relies on a map and we cannot garuntee + // because the order of the toppings relies on a map and we cannot guarantee // that the toppings will always be in the same order. tests.Compare(t, buf.String()[:76], expected[:76]) tests.Compare(t, buf.String()[147:], expected[147:]) @@ -127,25 +111,19 @@ func TestPrintItems(t *testing.T) { } func TestPrintMenu(t *testing.T) { + tests.InitHelpers(t) menu, err := testStore.Menu() - if err != nil { - t.Error(err) - } + tests.Check(err) buf := new(bytes.Buffer) SetOutput(buf) defer ResetOutput() - err = PrintMenu(menu.Categorization.Food, 0, menu) - if err != nil { - t.Error() - } + tests.Check(PrintMenu(menu.Categorization.Food, 0, menu)) if buf.Len() < 9000 { t.Error("the menu output seems a bit too short") } buf.Reset() - if err = PrintMenu(menu.Categorization.Preconfigured, 0, menu); err != nil { - t.Error(err) - } + tests.Check(PrintMenu(menu.Categorization.Preconfigured, 0, menu)) if buf.Len() < 1000 { t.Error("menu output is too short") } diff --git a/cmd/internal/out/templates.go b/cmd/internal/out/templates.go index ee29338..63dfaf2 100644 --- a/cmd/internal/out/templates.go +++ b/cmd/internal/out/templates.go @@ -14,17 +14,19 @@ func tmpl(w io.Writer, tmplt string, a interface{}) (err error) { } var defaultOrderTmpl = `{{ .OrderName }} - products:{{ range .Products }} - {{.Name}} - code: {{.Code}} - options:{{ range $k, $v := .ReadableOptions }} - {{$k}}: {{$v}}{{else}}None{{end}} - quantity: {{.Qty}}{{end}} - storeID: {{.StoreID}} - method: {{.ServiceMethod}} - address: {{.Addr -}} +{{- $keycol := .KeyColor -}} +{{- $endcol := .EndColor }} + {{.KeyColor}}products{{.EndColor}}:{{ range .Products }} + {{$keycol}}name{{$endcol}}: {{.Name}} + {{$keycol}}code{{$endcol}}: {{.Code}} + {{$keycol}}options{{$endcol}}:{{ range $k, $v := .ReadableOptions }} + {{$keycol}}{{$k}}{{$endcol}}: {{$v}}{{else}}None{{end}} + {{$keycol}}quantity{{$endcol}}: {{.Qty}}{{end}} + {{.KeyColor}}storeID{{.EndColor}}: {{.StoreID}} + {{.KeyColor}}method{{.EndColor}}: {{.ServiceMethod}} + {{.KeyColor}}address{{.EndColor}}: {{.Addr -}} {{ if .Price }} - price: ${{ .Price -}} + {{.KeyColor}}price{{.EndColor}}: ${{ .Price -}} {{else}}{{end}} ` @@ -47,6 +49,6 @@ var itemTmpl = `{{.ItemName}} var productTmpl = ` Description: {{.Description}} Variants: {{.Variants}} - Avalable sides: {{ if not .AvailableSides }}none{{else}}{{.AvailableSides}}{{end}} - Avalable toppings: {{ if not .AvailableToppings }}none{{else}}{{.AvailableToppings}}{{end}} + Available sides: {{ if not .AvailableSides }}none{{else}}{{.AvailableSides}}{{end}} + Available toppings: {{ if not .AvailableToppings }}none{{else}}{{.AvailableToppings}}{{end}} ` diff --git a/cmd/internal/util.go b/cmd/internal/util.go new file mode 100644 index 0000000..a9a9b4f --- /dev/null +++ b/cmd/internal/util.go @@ -0,0 +1,72 @@ +package internal + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/harrybrwn/apizza/dawg" +) + +// YesOrNo asks a yes or no question. +func YesOrNo(in *os.File, msg string) bool { + var res string + fmt.Printf("%s ", msg) + _, err := fmt.Fscan(in, &res) + if err != nil { + return false + } + + switch strings.ToLower(res) { + case "y", "yes", "si": + return true + } + return false +} + +// AddTopping parses and adds a topping from the raw string. +// +// formated as :: +// name is the only one that is required. +func AddTopping(topStr string, p dawg.Item) error { + var side, amount string + + topping := strings.Split(topStr, ":") + + // assuming strings.Split cannot return zero length array + if topping[0] == "" || len(topping) > 3 { + return errors.New("incorrect topping format") + } + + // TODO: need to check for bad values and use appropriate error handling + if len(topping) == 1 { + side = dawg.ToppingFull + } else if len(topping) >= 2 { + side = topping[1] + switch strings.ToLower(side) { + case "left": + side = dawg.ToppingLeft + case "right": + side = dawg.ToppingRight + case "full": + side = dawg.ToppingFull + default: + return errors.New("invalid topping side, should be either 'full', 'left', or 'right'") + } + } + amount = "1.0" + if len(topping) == 3 { + amount = topping[2] + } + + switch amount { + case "1", "2": + amount += ".0" + case "0.5", "1.0", "1.5", "2.0": + break + default: + return errors.New("invalid topping amount, should be any of '0.5', '1.0', '1.5', or '2.0'") + } + return p.AddTopping(topping[0], side, amount) +} diff --git a/cmd/menu.go b/cmd/menu.go index c2583dc..1ae5ee2 100644 --- a/cmd/menu.go +++ b/cmd/menu.go @@ -19,13 +19,13 @@ import ( "io" "os/exec" "strings" - "time" "unicode/utf8" "github.com/harrybrwn/apizza/cmd/cli" "github.com/harrybrwn/apizza/cmd/client" "github.com/harrybrwn/apizza/cmd/internal/data" "github.com/harrybrwn/apizza/cmd/internal/out" + "github.com/harrybrwn/apizza/cmd/opts" "github.com/harrybrwn/apizza/pkg/cache" "github.com/harrybrwn/apizza/pkg/errs" "github.com/spf13/cobra" @@ -33,8 +33,6 @@ import ( "github.com/harrybrwn/apizza/dawg" ) -var menuUpdateTime = 12 * time.Hour - type menuCmd struct { cli.CliCommand data.MenuCacher @@ -56,7 +54,7 @@ type menuCmd struct { func (c *menuCmd) Run(cmd *cobra.Command, args []string) error { if err := c.db.UpdateTS("menu", c); err != nil { - return err + cmd.Println(err) } out.SetOutput(c.Output()) defer out.ResetOutput() @@ -88,7 +86,7 @@ func (c *menuCmd) Run(cmd *cobra.Command, args []string) error { return nil } - // print menu handles most of the menu command's flags + // printmenu and pageMenu handle most of the menu command's flags if c.page { return c.pageMenu(strings.ToLower(c.category)) } @@ -104,7 +102,6 @@ func NewMenuCmd(b cli.Builder) cli.CliCommand { preconfigured: false, showCategories: false, } - // TODO: this will not work with a global service or address flag if app, ok := b.(*App); ok { c.StoreFinder = app } else { @@ -112,18 +109,20 @@ func NewMenuCmd(b cli.Builder) cli.CliCommand { } c.CliCommand = b.Build("menu ", "View the Dominos menu.", c) - c.MenuCacher = data.NewMenuCacher(menuUpdateTime, b.DB(), c.Store) + c.MenuCacher = data.NewMenuCacher(opts.MenuUpdateTime, b.DB(), c.Store) c.SetOutput(b.Output()) c.Cmd().Long = `This command will show the dominos menu. -To show a subdivition of the menu, give an item or +To show a subdivision of the menu, give an item or category to the --category and --item flags or give them as an argument to the command itself.` + c.Cmd().ValidArgsFunction = c.categoryCompletion + flags := c.Flags() flags.BoolVarP(&c.all, "all", "a", c.all, "show the entire menu") - flags.BoolVarP(&c.verbose, "verbose", "v", false, "print the menu verbosly") + flags.BoolVarP(&c.verbose, "verbose", "v", false, "print the menu verbosely") flags.BoolVar(&c.page, "page", false, "pipe the menu to a pager") flags.StringVarP(&c.item, "item", "i", "", "show info on the menu item given") @@ -140,12 +139,7 @@ func (c *menuCmd) printMenu(w io.Writer, name string) error { out.SetOutput(w) defer out.ResetOutput() menu := c.Menu() - var allCategories = menu.Categorization.Food.Categories - if c.preconfigured { - allCategories = menu.Categorization.Preconfigured.Categories - } else if c.all { - allCategories = append(allCategories, menu.Categorization.Preconfigured.Categories...) - } + var allCategories = c.getCategories(menu) if len(name) > 0 { for _, cat := range allCategories { @@ -155,11 +149,8 @@ func (c *menuCmd) printMenu(w io.Writer, name string) error { } return fmt.Errorf("could not find %s", name) } else if c.showCategories { - for _, cat := range allCategories { - if cat.Name != "" { - fmt.Fprintln(w, strings.ToLower(cat.Name)) - } - } + cats, _ := c.categoryCompletion(nil, []string{}, "") + fmt.Fprintln(w, strings.Join(cats, "\n")) return nil } @@ -201,12 +192,38 @@ func (c *menuCmd) pageMenu(category string) error { go func() { defer stdin.Close() err = c.printMenu(stdin, strings.ToLower(category)) // still works with an empty string - errs.Handle(err, "io Error", 1) + errs.StopNow(err, "io Error", 1) }() return less.Run() } +func (c *menuCmd) getCategories(m *dawg.Menu) []dawg.MenuCategory { + var all = m.Categorization.Food.Categories + if c.preconfigured { + all = m.Categorization.Preconfigured.Categories + } else if c.all { + all = append(all, m.Categorization.Preconfigured.Categories...) + } + return all +} + +func (c *menuCmd) categoryCompletion( + cmd *cobra.Command, + args []string, + toComplete string, +) ([]string, cobra.ShellCompDirective) { + all := c.getCategories(c.Menu()) + categories := make([]string, 0, len(all)) + for _, cat := range all { + if cat.Name == "" { + continue + } + categories = append(categories, strings.ToLower(cat.Name)) + } + return categories, cobra.ShellCompDirectiveNoFileComp +} + func printToppingCategory(name string, toppings map[string]dawg.Topping, w io.Writer) { fmt.Fprintln(w, " ", name) indent := strings.Repeat(" ", 4) diff --git a/cmd/menu_test.go b/cmd/menu_test.go index f2f3e09..5f2d7c2 100644 --- a/cmd/menu_test.go +++ b/cmd/menu_test.go @@ -4,29 +4,23 @@ import ( "testing" "github.com/harrybrwn/apizza/cmd/internal/cmdtest" + "github.com/harrybrwn/apizza/pkg/tests" ) func TestMenuRun(t *testing.T) { + tests.InitHelpers(t) r := cmdtest.NewRecorder() defer r.CleanUp() c := NewMenuCmd(r).(*menuCmd) - if err := c.Run(c.Cmd(), []string{}); err != nil { - t.Error(err) - } + tests.Check(c.Run(c.Cmd(), []string{})) c.item = "not a thing" - if err := c.Run(c.Cmd(), []string{}); err == nil { - t.Error("should raise error") - } + tests.Exp(c.Run(c.Cmd(), []string{})) c.item = "10SCREEN" - if err := c.Run(c.Cmd(), []string{}); err != nil { - t.Error(err) - } + tests.Check(c.Run(c.Cmd(), []string{})) c.item = "" c.toppings = true - if err := c.Run(c.Cmd(), []string{}); err != nil { - t.Error(err) - } + tests.Check(c.Run(c.Cmd(), []string{})) } func TestFindProduct(t *testing.T) { @@ -38,7 +32,7 @@ func TestFindProduct(t *testing.T) { t.Error(err) } c.all = true - if err := c.printMenu(c.Output(), ""); err != nil { // yes, this is supposd to be an empty string... in this case + if err := c.printMenu(c.Output(), ""); err != nil { // yes, this is supposed to be an empty string... in this case t.Error(err) } r.ClearBuf() diff --git a/cmd/opts/common.go b/cmd/opts/common.go index 9777975..6fbc8c5 100644 --- a/cmd/opts/common.go +++ b/cmd/opts/common.go @@ -1,6 +1,13 @@ package opts -import "github.com/spf13/pflag" +import ( + "time" + + "github.com/spf13/pflag" +) + +// MenuUpdateTime is the time a menu is persistant in cache +const MenuUpdateTime = 12 * time.Hour // CliFlags for the root apizza command. type CliFlags struct { @@ -14,11 +21,11 @@ type CliFlags struct { // Install the RootFlags func (rf *CliFlags) Install(persistflags *pflag.FlagSet) { - persistflags.BoolVar(&rf.ClearCache, "clear-cache", false, "delete the database") + rf.ClearCache = false persistflags.BoolVar(&rf.ResetMenu, "delete-menu", false, "delete the menu stored in cache") - persistflags.StringVar(&rf.LogFile, "log", "", "set a log file (found in ~/.apizza/logs)") + persistflags.StringVar(&rf.LogFile, "log", "", "set a log file (found in ~/.config/apizza/logs)") - persistflags.StringVar(&rf.Address, "address", rf.Address, "use a specific address") + persistflags.StringVarP(&rf.Address, "address", "A", rf.Address, "an address name stored with 'apizza address --new'") persistflags.StringVar(&rf.Service, "service", rf.Service, "select a Dominos service, either 'Delivery' or 'Carryout'") } diff --git a/dawg/.gitignore b/dawg/.gitignore new file mode 100644 index 0000000..1a74edd --- /dev/null +++ b/dawg/.gitignore @@ -0,0 +1,4 @@ +!testdata/menu.json +!testdata/store-locator.json +!testdata/store.json +!testdata/order-meta.json diff --git a/dawg/address.go b/dawg/address.go index cc98af2..4db087b 100644 --- a/dawg/address.go +++ b/dawg/address.go @@ -39,7 +39,7 @@ func parse(raw []byte) ([][]byte, error) { if len(res) > 0 { return res[0], nil } - return nil, errors.New("address parsing error") + return nil, errors.New("could not parse address string") } // Address is a guid for how addresses should be used as input @@ -102,6 +102,14 @@ func StreetAddrFromAddress(addr Address) *StreetAddr { } } +// Equal will test if an s is the same as the Address given. +func (s *StreetAddr) Equal(a Address) bool { + return s.City() == a.City() && + s.LineOne() == a.LineOne() && + s.StateCode() == a.StateCode() && + s.Zip() == a.Zip() +} + // LineOne gives the street in the following format // // diff --git a/dawg/auth.go b/dawg/auth.go index 1a91d7f..aa9844f 100644 --- a/dawg/auth.go +++ b/dawg/auth.go @@ -3,21 +3,18 @@ package dawg import ( "bytes" "encoding/json" - "errors" "fmt" "io" "io/ioutil" "net/http" "net/url" "strings" - "time" + + "github.com/harrybrwn/apizza/dawg/internal/auth" ) -type auth struct { - username string - password string - token *token - cli *client +type doer interface { + Do(*http.Request) (*http.Response, error) } const ( @@ -25,66 +22,43 @@ const ( loginEndpoint = "https://order.dominos.com/power/login" ) -var oauthURL = &url.URL{ - Scheme: "https", - Host: "api.dominos.com", - Path: "/as/token.oauth2", -} +var ( + // As of May 9, 2020, it was discovered that the authentication endpoint was changed from, + // "api.dominos.com/as/token.oauth2" + // to, + // "authproxy.dominos.com/auth-proxy-service/login". + // I am documenting this change just in case it every changed back in the future. + // + // TODO: See comment above. Possible solutions are try both oauth endpoints or let users specify which to use. + oauthURL = &url.URL{ + Scheme: "https", + Host: "authproxy.dominos.com", + Path: "/auth-proxy-service/login", + } -var loginURL = &url.URL{ - Scheme: "https", - Host: orderHost, - Path: "/power/login", -} + loginURL = &url.URL{ + Scheme: "https", + Host: orderHost, + Path: "/power/login", + } +) -func newauth(username, password string) (*auth, error) { +func authorize(c *http.Client, username, password string) error { tok, err := gettoken(username, password) if err != nil { - return nil, err + return err } - a := &auth{ - token: tok, - username: username, - password: password, - cli: &client{ - host: orderHost, - Client: &http.Client{ - Transport: tok, - Timeout: 60 * time.Second, - CheckRedirect: noRedirects, - }, - }, + if c.Transport != nil { + tok.SetTransport(c.Transport) } - return a, nil + c.Transport = tok + return nil } var noRedirects = func(r *http.Request, via []*http.Request) error { return http.ErrUseLastResponse } -const tokenHost = "api.dominos.com" - -type token struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token,omitempty"` - Type string `json:"token_type"` - - // ExpiresIn is the time in seconds that it takes for the token to - // expire. - ExpiresIn int `json:"expires_in"` - - transport http.RoundTripper -} - -func (t *token) authorization() string { - return fmt.Sprintf("%s %s", t.Type, t.AccessToken) -} - -func (t *token) RoundTrip(req *http.Request) (*http.Response, error) { - req.Header.Add("Authorization", t.authorization()) - return t.transport.RoundTrip(req) -} - var scopes = []string{ "customer:card:read", "customer:profile:read:extended", @@ -103,44 +77,52 @@ var scopes = []string{ "easyOrder:read", } -func gettoken(username, password string) (*token, error) { +func gettoken(username, password string) (*auth.Token, error) { data := url.Values{ - "grant_type": {"password"}, - "client_id": {"nolo-rm"}, // nolo-rm if you want a refresh token, or just nolo for temporary token - "scope": {strings.Join(scopes, " ")}, - "username": {username}, - "password": {password}, + "grant_type": {"password"}, + "client_id": {"nolo-rm"}, // nolo-rm if you want a refresh token, or just nolo for temporary token + "validator_id": {"VoldemortCredValidator"}, + "scope": {strings.Join(scopes, " ")}, + "username": {username}, + "password": {password}, } - req := newAuthRequest(oauthURL, data) + req := newPostReq(oauthURL, data) resp, err := orderClient.Do(req) if err != nil { return nil, err } - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf( - "dawg.gettoken: bad status code %d", resp.StatusCode) + defer resp.Body.Close() + + result := struct { + *auth.Token + *auth.Error + }{Token: auth.NewToken(), Error: nil} + + if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err } - tok := &token{transport: http.DefaultTransport} - return tok, errpair(unmarshalToken(resp.Body, tok), resp.Body.Close()) + if result.Error != nil { + return nil, result.Error + } + return result.Token, nil } -func (a *auth) login() (*UserProfile, error) { +func login(c *client) (*UserProfile, error) { data := url.Values{ "loyaltyIsActive": {"true"}, "rememberMe": {"true"}, - "u": {a.username}, - "p": {a.password}, } - req := newAuthRequest(loginURL, data) - res, err := a.cli.Do(req) + req := newPostReq(loginURL, data) + res, err := c.Do(req) if err != nil { return nil, err } defer res.Body.Close() + if !isjson(res) { + return nil, fmt.Errorf("did not get a json response; got %s", res.Header.Get("Content-Type")) + } - profile := new(UserProfile) - profile.auth = a - + profile := &UserProfile{cli: c} b, err := ioutil.ReadAll(res.Body) if err = errpair(err, dominosErr(b)); err != nil { return nil, err @@ -148,7 +130,7 @@ func (a *auth) login() (*UserProfile, error) { return profile, json.Unmarshal(b, profile) } -func newAuthRequest(u *url.URL, vals url.Values) *http.Request { +func newPostReq(u *url.URL, vals url.Values) *http.Request { return &http.Request{ Method: "POST", Proto: "HTTP/1.1", @@ -157,10 +139,7 @@ func newAuthRequest(u *url.URL, vals url.Values) *http.Request { Host: u.Host, Header: http.Header{ "Content-Type": { - "application/x-www-form-urlencoded; charset=UTF-8"}, - "User-Agent": { - "Apizza Dominos API Wrapper for Go " + time.Now().UTC().String()}, - }, + "application/x-www-form-urlencoded; charset=UTF-8"}}, URL: u, Body: ioutil.NopCloser(strings.NewReader(vals.Encode())), } @@ -172,27 +151,40 @@ type client struct { } func (c *client) do(req *http.Request) ([]byte, error) { + return do(c.Client, req) +} + +func do(d doer, req *http.Request) ([]byte, error) { var buf bytes.Buffer - req.Header.Add( - "User-Agent", - "Dominos API Wrapper for GO - "+time.Now().String(), - ) - resp, err := c.Do(req) + resp, err := d.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("dawg.client.do: bad status code %d", resp.StatusCode) + return nil, fmt.Errorf("dawg.do: bad status code %s", resp.Status) } - _, err = buf.ReadFrom(resp.Body) - if bytes.HasPrefix(bytes.ToLower(buf.Bytes()[:15]), []byte("")) { - return nil, errpair(err, errors.New("got html response")) + if !isjson(resp) { + return nil, fmt.Errorf("did not get json response, got %s", resp.Header.Get("Content-Type")) } + _, err = buf.ReadFrom(resp.Body) return buf.Bytes(), err } +func (c *client) dojson(v interface{}, r *http.Request) (err error) { + return dojson(c, v, r) +} + +func dojson(d doer, v interface{}, r *http.Request) (err error) { + resp, err := d.Do(r) + if err != nil { + return err + } + defer resp.Body.Close() + return json.NewDecoder(resp.Body).Decode(v) +} + func (c *client) get(path string, params URLParam) ([]byte, error) { if params == nil { params = &Params{} @@ -211,6 +203,24 @@ func (c *client) get(path string, params URLParam) ([]byte, error) { }) } +func get(d doer, host, path string, params URLParam) (*http.Response, error) { + if params == nil { + params = &Params{} + } + return d.Do(&http.Request{ + Method: "GET", + Host: host, + Proto: "HTTP/1.1", + Header: make(http.Header), + URL: &url.URL{ + Scheme: "https", + Host: host, + Path: path, + RawQuery: params.Encode(), + }, + }) +} + func (c *client) post(path string, params URLParam, r io.Reader) ([]byte, error) { if params == nil { params = &Params{} @@ -234,7 +244,7 @@ func (c *client) post(path string, params URLParam, r io.Reader) ([]byte, error) }) } -func unmarshalToken(r io.ReadCloser, t *token) error { +func unmarshalToken(r io.ReadCloser, t *auth.Token) error { buf := new(bytes.Buffer) defer r.Close() @@ -243,7 +253,13 @@ func unmarshalToken(r io.ReadCloser, t *token) error { if err != nil { return err } - return newTokenErr(buf.Bytes()) + e := &tokenError{} + // if there is no token error the the json parsing will fail + json.Unmarshal(buf.Bytes(), e) + if len(e.Err) > 0 || len(e.ErrorDesc) > 0 { + return e + } + return nil } type tokenError struct { @@ -255,12 +271,24 @@ func (e *tokenError) Error() string { return fmt.Sprintf("%s: %s", e.Err, e.ErrorDesc) } -func newTokenErr(b []byte) error { +func newTokErr(r io.Reader) error { e := &tokenError{} - // if there is no error the the json parsing will fail - json.Unmarshal(b, e) - if len(e.Err) > 0 { + if err := json.NewDecoder(r).Decode(e); err != nil { + return err + } + if len(e.Err) > 0 || len(e.ErrorDesc) > 0 { return e } return nil } + +func isjson(r *http.Response) bool { + contentType := r.Header.Get("Content-Type") + types := strings.Split(contentType, ";") + for _, typ := range types { + if typ == "application/json" { + return true + } + } + return false +} diff --git a/dawg/auth_test.go b/dawg/auth_test.go index b37c75b..c2dc29d 100644 --- a/dawg/auth_test.go +++ b/dawg/auth_test.go @@ -1,40 +1,54 @@ package dawg import ( - "fmt" - "io/ioutil" + "errors" "net/http" - "net/url" "os" "strings" "testing" "time" + + "github.com/harrybrwn/apizza/dawg/internal/auth" + "github.com/harrybrwn/apizza/pkg/tests" ) func TestBadCreds(t *testing.T) { + t.Skip("test takes too long") // swap the default client with one that has a // 10s timeout then defer the cleanup. - defer swapclient(10)() + defer swapclient(8)() + tests.InitHelpers(t) - tok, err := gettoken("no", "and no") - if err == nil { - t.Error("expected an error") + err := authorize(orderClient.Client, "5uup;hrg];ht8bijer$u9tot", "hurieahgr9[0249eingurivja") + tests.Exp(err) + if _, ok := orderClient.Transport.(*auth.Token); ok { + t.Error("bad authorization should not set the client transport to a token") } + tok, err := gettoken("no", "and no") + tests.Exp(err) if tok != nil { - t.Error("expected nil token") + t.Errorf("expected nil %T", tok) } - - tok, err = gettoken("", "") - if err == nil { - t.Error("expected an error") + if _, ok := err.(*auth.Error); !ok { + t.Errorf("expected an *auth.Error got %T:\n%v", err, err) } - if tok != nil { - t.Error("expected nil token") + user, err := login(orderClient) + tests.Exp(err) + if user != nil { + t.Errorf("expected nil %T", user) } - tok, err = gettoken("5uup;hrg];ht8bijer$u9tot", "hurieahgr9[0249eingurivja") - if err == nil { - t.Error("wow i accidently cracked someone's password:", tok) + username, password, ok := gettestcreds() + if !ok { + t.Skip() + } + orderClient.Client.Transport = newRoundTripper(func(*http.Request) error { + return errors.New("this should make the client fail") + }) + tok, err = gettoken(username, password) + tests.Exp(err) + if tok != nil { + t.Errorf("expected nil %T", tok) } } @@ -46,18 +60,7 @@ func gettestcreds() (string, string, bool) { return u, p, true } -var ( - testAuth *auth - testUser *UserProfile -) - -func getTestAuth(uname, pass string) (*auth, error) { - var err error - if testAuth == nil { - testAuth, err = newauth(uname, pass) - } - return testAuth, err -} +var testUser *UserProfile func getTestUser(uname, pass string) (*UserProfile, error) { var err error @@ -74,18 +77,24 @@ func getTestUser(uname, pass string) (*UserProfile, error) { // halt my tests for like 60+ seconds per test. // // usage: -// defer swapclient(15)() +// defer swapclient(15)() // this will call swapclient and defer the cleanup function // that it returns so that the default client is reset. // // if someone is actually reading this, im sorry, i know this // is not very go-like, i know its hacky... sorry func swapclient(timeout int) func() { + dur := time.Duration(timeout) * time.Second copyclient := orderClient orderClient = &client{ host: orderHost, Client: &http.Client{ - Timeout: time.Duration(timeout) * time.Second, + Timeout: dur, + Transport: &http.Transport{ + TLSHandshakeTimeout: dur, + IdleConnTimeout: dur, + ResponseHeaderTimeout: dur, + }, CheckRedirect: noRedirects, }, } @@ -97,241 +106,41 @@ func TestToken(t *testing.T) { if !ok { t.Skip() } - // swapclient is called first and the cleaup - // function it returns is defered. - defer swapclient(10)() + // swapclient is called first and the cleanup + // function it returns is deferred. + // defer swapclient(8)() + client, mux, server := testServer() + defer server.Close() + defer swapClientWith(client)() + addUserHandlers(t, mux) + tests.InitHelpers(t) tok, err := gettoken(username, password) - if err != nil { - fmt.Printf("%T\n", err) - t.Errorf("%T\n", err) - t.Error(err) - } + tests.Check(err) if tok == nil { - t.Fatal("nil token") + t.Fatalf("got nil %T got %v", tok, tok) } if len(tok.AccessToken) == 0 { - t.Error("didnt get a auth token") - } - if !strings.HasPrefix(tok.authorization(), "Bearer ") { - t.Error("bad auth format") - } - if tok.transport == nil { - t.Error("token should have a transport") + t.Error("didn't get a auth token") } if tok.Type != "Bearer" { t.Error("these tokens are usually bearer tokens") } -} - -func TestAuth(t *testing.T) { - username, password, ok := gettestcreds() - if !ok { - t.Skip() - } - defer swapclient(10)() - - auth, err := getTestAuth(username, password) - if err != nil { - t.Error(err) - } - if auth == nil { - t.Fatal("got nil auth") - } - if auth.token == nil { - t.Fatal("needs token") - } - if len(auth.username) == 0 { - t.Error("didnt save username") - } - if len(auth.password) == 0 { - t.Error("didnt save password") - } - if auth.cli == nil { - t.Fatal("needs to have client") - } - if len(auth.token.AccessToken) == 0 { - t.Error("no access token") - } - - user, err := auth.login() - if err != nil { - t.Error(err) - } - if user == nil { - t.Fatal("got nil user-profile") - } - user.AddAddress(testAddress()) - user.Addresses[0].StreetNumber = "" - user.Addresses[0].StreetName = "" - user.AddAddress(user.Addresses[0]) - a1 := user.Addresses[0] - a2 := user.Addresses[1] - if a1.StreetName != a2.StreetName { - t.Error("did not copy address name correctly") - } - if a1.StreetNumber != a2.StreetNumber { - t.Error("did not copy address number correctly") - } - a1.Street = "" - if user.Addresses[0].LineOne() != a2.LineOne() { - t.Error("line one for UserAddress is broken") - } - - store, err := user.NearestStore("Delivery") - if err != nil { - t.Error(err) - } - if store.cli == nil { - t.Fatal("store did not get a client") - } - if store.cli.host != "order.dominos.com" { - t.Error("store client has the wrong host") - } - req := &http.Request{ - Method: "GET", Host: store.cli.host, Proto: "HTTP/1.1", - URL: &url.URL{ - Scheme: "https", Host: store.cli.host, - Path: fmt.Sprintf("/power/store/%s/menu", store.ID), - RawQuery: (&Params{"lang": DefaultLang, "structured": "true"}).Encode()}, - } - res, err := store.cli.Do(req) - if err != nil { - t.Error(err) - } - defer res.Body.Close() - authhead := res.Request.Header.Get("Authorization") - if authhead != auth.token.authorization() { - t.Error("store client didnt get the token") - } - b, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Error(err) - } - if len(b) == 0 { - t.Error("zero length response") - } - menu, err := store.Menu() - if err != nil { - t.Error(err) - } - if menu == nil { - t.Error("got nil menu") - } - o := store.NewOrder() - if o == nil { - t.Error("nil order") - } - _, err = o.Price() - if err != nil { - t.Error(err) - } -} - -func TestAuth_Err(t *testing.T) { - defer swapclient(10)() - if orderClient.Client.Timeout != time.Duration(10)*time.Second { - t.Error("client was not swapped") - } - - a, err := newauth("not a", "valid password") - if err == nil { - t.Error("expected an error") - } - if a != nil { - t.Error("expected a nil auth") - } - a = &auth{ - username: "not a", - password: "valid password", - token: &token{}, // assume we alread have a token - cli: &client{ - host: "order.dominos.com", - Client: &http.Client{ - Timeout: 15 * time.Second, - CheckRedirect: noRedirects, - }, - }, - } - - user, err := a.login() - if err == nil { - t.Error("expected an error") - } - if user != nil { - t.Errorf("expected a nil user: %+v", user) - } - a.cli.host = "invalid_host.com" - user, err = a.login() - if err == nil { - t.Error("expedted an error") - } - if user != nil { - t.Error("user should still be nil") - } -} - -func TestSignIn(t *testing.T) { - username, password, ok := gettestcreds() - if !ok { - t.Skip() - } - defer swapclient(10)() - - user, err := SignIn(username, password) - if err != nil { - t.Error(err) - } - if user == nil { - t.Fatal("got nil user from SignIn") + if len(tok.AccessToken) == 0 { + t.Error("did not get the access token") } - testUser = user } -func TestAuthClient(t *testing.T) { - username, password, ok := gettestcreds() - if !ok { - t.Skip() - } - defer swapclient(10)() - - auth, err := getTestAuth(username, password) - if auth == nil { - t.Fatal("got nil auth") - } - - if auth.cli == nil { - t.Error("client should not be nil") - } - err = auth.cli.CheckRedirect(nil, nil) - if err == nil { - t.Error("order Client should not allow redirects") - } - err = auth.cli.CheckRedirect(&http.Request{}, []*http.Request{}) - if err == nil { - t.Error("expected error") - } - tok, err := gettoken("bad", "creds") - if err == nil { - t.Error("should return error") - } - - req := newAuthRequest(oauthURL, url.Values{}) - resp, err := http.DefaultClient.Do(req) - if err != nil { - t.Error(err) - } - tok = &token{} - err = unmarshalToken(resp.Body, tok) - if err == nil { - t.Error("expected error") +func TestToken_Err(t *testing.T) { + tokErr := newTokErr(strings.NewReader(`{"error":"","error_description":""}`)) + if tokErr != nil { + t.Error("this error should be nil") } - if e, ok := err.(*tokenError); !ok { - t.Error("expected a *tokenError as the error") - } else if e.Error() != fmt.Sprintf("%s: %s", e.Err, e.ErrorDesc) { - t.Error("wrong error message") + tokErr = newTokErr(strings.NewReader(`{"error":"test","error_description":"test"}`)) + if tokErr == nil { + t.Error("this error should not be nil") } - if IsOk(err) { - t.Error("this shouldnt happen") + if tokErr.Error() != "test: test" { + t.Error("got wrong error msg") } } diff --git a/dawg/dawg.go b/dawg/dawg.go deleted file mode 100644 index e080f80..0000000 --- a/dawg/dawg.go +++ /dev/null @@ -1,11 +0,0 @@ -// Package dawg (Dominos API Wrapper for Go) is a package that allows a go programmer -// to interact with the dominos web api. -package dawg - -const ( - // DefaultLang is the package language variable - DefaultLang = "en" - - // host = "order.dominos.com" - orderHost = "order.dominos.com" -) diff --git a/dawg/dawg_test.go b/dawg/dawg_test.go index a95366c..4c269cc 100644 --- a/dawg/dawg_test.go +++ b/dawg/dawg_test.go @@ -8,9 +8,48 @@ import ( "math/rand" "net" "net/http" + "net/http/httptest" + "net/url" "testing" + "time" + + "github.com/harrybrwn/apizza/pkg/tests" ) +func testServer() (*http.Client, *http.ServeMux, *httptest.Server) { + m := http.NewServeMux() + srv := httptest.NewServer(m) + u, err := url.Parse(srv.URL) + tr := &TestTransport{ + host: u.Host, + rt: &http.Transport{ + Proxy: func(r *http.Request) (*url.URL, error) { + return u, err + }, + }} + c := &http.Client{Transport: tr} + return c, m, srv +} + +type TestTransport struct { + host string + rt http.RoundTripper +} + +func (tt *TestTransport) RoundTrip(r *http.Request) (*http.Response, error) { + r.URL.Scheme = "http" + if r.URL.Host == "" { + r.URL.Host = tt.host + } + return tt.rt.RoundTrip(r) +} + +func swapClientWith(c *http.Client) func() { + old := orderClient + orderClient = &client{Client: c} + return func() { orderClient = old } +} + func TestFormat(t *testing.T) { url := format("https://order.dominos.com/power/%s", "store-locator") expected := "https://order.dominos.com/power/store-locator" @@ -20,6 +59,7 @@ func TestFormat(t *testing.T) { } func TestOrderAddressConvertion(t *testing.T) { + tests.InitHelpers(t) exp := &StreetAddr{StreetNum: "1600", StreetName: "Pennsylvania Ave.", Street: "1600 Pennsylvania Ave.", CityName: "Washington", State: "DC", Zipcode: "20500", AddrType: "House"} @@ -32,27 +72,16 @@ func TestOrderAddressConvertion(t *testing.T) { } res := StreetAddrFromAddress(addr) - if res.City() != exp.City() { - t.Error("wrong city") - } - if res.LineOne() != exp.LineOne() { - t.Error("wrong lineone") - } - if res.StateCode() != exp.StateCode() { - t.Error("wrong state code") - } - if res.Zip() != exp.Zip() { - t.Error("wrong zip code") - } - if res.StreetNum != exp.StreetNum { - t.Error("wrong street number") - } - if res.StreetName != exp.StreetName { - t.Error("wrong street name") - } + tests.StrEq(res.City(), exp.City(), "wrong city") + tests.StrEq(res.LineOne(), exp.LineOne(), "wrong lineone") + tests.StrEq(res.StateCode(), exp.StateCode(), "wrong state code") + tests.StrEq(res.Zip(), exp.Zip(), "wrong zip code") + tests.StrEq(res.StreetNum, exp.StreetNum, "wrong street number") + tests.StrEq(res.StreetName, exp.StreetName, "wrong street name") } func TestParseAddressTable(t *testing.T) { + tests.InitHelpers(t) var cases = []struct { raw string expected StreetAddr @@ -73,51 +102,32 @@ func TestParseAddressTable(t *testing.T) { for _, tc := range cases { addr, err := ParseAddress(tc.raw) - if err != nil { - t.Error(err) - } - if addr.StreetNum != tc.expected.StreetNum { - t.Error("wrong street num") - } - if addr.Street != tc.expected.Street { - t.Error("wrong street") - } - if addr.CityName != tc.expected.CityName { - t.Error("wrong city") - } - if addr.State != tc.expected.State { - t.Error("wrong state") - } - if addr.Zipcode != tc.expected.Zipcode { - t.Error("wrong zip") - } + tests.Check(err) + tests.StrEq(addr.StreetNum, tc.expected.StreetNum, "wrong street num") + tests.StrEq(addr.Street, tc.expected.Street, "wrong street") + tests.StrEq(addr.CityName, tc.expected.CityName, "wrong city") + tests.StrEq(addr.State, tc.expected.State, "wrong state") + tests.StrEq(addr.Zipcode, tc.expected.Zipcode, "wrong zip") } } func TestNetworking_Err(t *testing.T) { + t.Skip("this test takes way too long") + tests.InitHelpers(t) + defer swapclient(10)() _, err := orderClient.get("/", nil) - if err == nil { - t.Error("expected error") - } + tests.Exp(err) _, err = orderClient.get("/invalid path", nil) - if err == nil { - t.Error("expected error") - } + tests.Exp(err) b, err := orderClient.post("/invalid path", nil, bytes.NewReader(make([]byte, 1))) + tests.Exp(err) if len(b) != 0 { - t.Error("exepcted zero length response") - } - if err == nil { - t.Error("expected error") + t.Error("expected zero length response") } _, err = orderClient.post("invalid path", nil, bytes.NewReader(nil)) - if err == nil { - t.Error("expected error") - } + tests.Exp(err) _, err = orderClient.post("/power/price-order", nil, bytes.NewReader([]byte{})) - if err == nil { - t.Error("expected error") - } + tests.Exp(err) cli := &client{ Client: &http.Client{ Transport: &http.Transport{ @@ -125,42 +135,30 @@ func TestNetworking_Err(t *testing.T) { return nil, errors.New("stop") }, }, + Timeout: time.Second, }, } resp, err := cli.get("/power/store/4336/profile", nil) - if err == nil { - t.Error("expected error") - } + tests.Exp(err) if resp != nil { t.Error("should not have gotten any response data") } b, err = cli.post("/invalid path", nil, bytes.NewReader(make([]byte, 1))) - if err == nil { - t.Error("expected error") - } + tests.Exp(err) if b != nil { - t.Error("exepcted zero length response") + t.Error("expected zero length response") } req, err := http.NewRequest("GET", "https://www.google.com/", nil) - if err != nil { - t.Error(err) - } + tests.Check(err) resp, err = orderClient.do(req) + tests.Exp(err, "expected an error because we found an html page\n") if err == nil { t.Error("expected an error because we found an html page") - fmt.Println(string(resp)) - } else if err.Error() != "got html response" { - t.Error("got an unexpected error:", err.Error()) } - req, err = http.NewRequest("GET", "https://hjfkghfdjkhgfjkdhgjkdghfdjk.com", nil) - if err != nil { - t.Error(err) - } + tests.Check(err) resp, err = orderClient.do(req) - if err == nil { - t.Error("expected an error") - } + tests.Exp(err) } func TestDominosErrors(t *testing.T) { @@ -168,8 +166,8 @@ func TestDominosErrors(t *testing.T) { LanguageCode: "en", ServiceMethod: "Delivery", Products: []*OrderProduct{ - &OrderProduct{ - item: item{Code: "12SCREEN"}, + { + ItemCommon: ItemCommon{Code: "12SCREEN"}, Opts: map[string]interface{}{ "C": map[string]string{"1/1": "1"}, "P": map[string]string{"1/1": "1.5"}, @@ -243,6 +241,63 @@ func TestDominosErrorFailure(t *testing.T) { } } +func TestValidateCard(t *testing.T) { + tests.InitHelpers(t) + tsts := []struct { + c Card + valid bool + }{ + {NewCard("", "0125", 123), false}, + {NewCard("", "01/25", 123), false}, + {NewCard("370218180742397", "0123", 123), true}, + {NewCard("370218180742397", "01/23", 123), true}, + {NewCard("370218180742397", "1/23", 123), true}, + {NewCard("370218180742397", "01/02", 123), true}, + {NewCard("370218180742397", "13/21", 123), false}, + {NewCard("370218180742397", "0/21", 123), false}, + } + + for _, tc := range tsts { + if tc.valid { + if tc.c == nil { + t.Error("got nil card when it should be valid") + continue + } + tests.Check(ValidateCard(tc.c)) + } else { + tests.Exp(ValidateCard(tc.c), "expected an error:", tc.c, tc.c.ExpiresOn()) + } + } +} + +func TestParseDate(t *testing.T) { + tst := []struct { + s string + m, y int + }{ + {"01/25", 1, 2025}, + {"0125", 1, 2025}, + {"01/2025", 1, 2025}, + {"1/25", 1, 2025}, + {"1/2025", 1, 2025}, + {"012025", -1, -1}, // failure case + {"11/02", 11, 2002}, + {"11/2002", 11, 2002}, + {"11/2", 11, 202}, // failure case + } + var m, y int + for _, tc := range tst { + + m, y = parseDate(tc.s) + if m != tc.m { + t.Errorf("got the wrong month; want %d, got %d", tc.m, m) + } + if y != tc.y { + t.Errorf("got the wrong year; wand %d, got %d", tc.y, y) + } + } +} + func TestErrPair(t *testing.T) { tt := []struct { err error @@ -263,26 +318,63 @@ func TestErrPair(t *testing.T) { } } +var ( + testStore *Store + testMenu *Menu +) + func testingStore() *Store { - var service string + var ( + service string + err error + ) if rand.Intn(2) == 1 { service = "Carryout" } else { service = "Delivery" } - - s, err := NearestStore(testAddress(), service) - if err != nil { - panic(err) + if testStore == nil { + testStore, err = NearestStore(testAddress(), service) + if err != nil { + panic(err) + } } - return s + return testStore } func testingMenu() *Menu { - m, err := testingStore().Menu() - if err != nil { - panic(err) + var err error + if testMenu == nil { + testMenu, err = testingStore().Menu() + if err != nil { + panic(err) + } + } + return testMenu +} + +func storeLocatorHandlerFunc(t *testing.T) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + addr := testAddress() + if q.Get("c") != fmt.Sprintf("%s, %s %s", addr.City(), addr.StateCode(), addr.Zip()) { + t.Error("bad url query: \"c\"") + } + if q.Get("s") != addr.LineOne() { + t.Error("bad url query: \"s\"") + } + fileHandleFunc(t, "./testdata/store-locator.json")(w, r) + } +} + +func storeProfileHandlerFunc(t *testing.T) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Error("not a get req") + w.WriteHeader(500) + return + } + fileHandleFunc(t, "./testdata/store.json")(w, r) } - return m } diff --git a/dawg/doc.go b/dawg/doc.go new file mode 100644 index 0000000..2a35b65 --- /dev/null +++ b/dawg/doc.go @@ -0,0 +1,20 @@ +// Package dawg (Dominos API Wrapper for Go) is a package that allows a go programmer +// to interact with the dominos web api. +// +// The two main entry points in the package are the SignIn and NearestStore functions. +// +// SignIn is used if you have an account with dominos. +// user, err := dawg.SignIn(username, password) +// if err != nil { +// // handle error +// } +// +// NearestStore should be used if you just want to make a one time order. +// store, err := dawg.NearestStore(&address, dawg.Delivery) +// if err != nil { +// // handle error +// } +// +// To order anything from dominos you need to find a store, create an order, +// then send that order. +package dawg diff --git a/dawg/errors.go b/dawg/errors.go index 04b01a9..640d67b 100644 --- a/dawg/errors.go +++ b/dawg/errors.go @@ -3,6 +3,7 @@ package dawg import ( "bytes" "encoding/json" + "errors" "fmt" "github.com/mitchellh/mapstructure" @@ -20,7 +21,15 @@ const ( ) var ( - // Warnings is a package switch for turning warings on or off + // ErrBadService is returned if a service is needed but the service validation failed. + ErrBadService = errors.New("service must be either 'Delivery' or 'Carryout'") + + // ErrNoUserService is thrown when a user has no service method. + ErrNoUserService = errors.New("UserProfile has no service method (use user.SetServiceMethod)") +) + +var ( + // Warnings is a package switch for turning warnings on or off Warnings = false errCodes = map[int]string{ @@ -149,7 +158,7 @@ func isDominosErr(err error) (*DominosError, bool) { return e, true } -// because i want my errs.Pair function but i dont want to add it as a +// because i want my errs.Pair function but I don't want to add it as a // dependency to the dawg package in case i ever want to separate them. func errpair(first, second error) error { if first == nil || second == nil { diff --git a/dawg/examples_test.go b/dawg/examples_test.go index 7f4d6bc..5a60e2a 100644 --- a/dawg/examples_test.go +++ b/dawg/examples_test.go @@ -3,10 +3,41 @@ package dawg_test import ( "fmt" "log" + "os" "github.com/harrybrwn/apizza/dawg" ) +var ( + user = dawg.UserProfile{} + username = os.Getenv("DOMINOS_TEST_USER") + password = os.Getenv("DOMINOS_TEST_PASS") + + address = dawg.StreetAddr{ + Street: "600 Mountain Ave bldg 5", + CityName: "New Providence", + State: "NJ", + Zipcode: "07974", + AddrType: "Business", + } +) + +func Example_getStore() { + // This can be anything that satisfies the dawg.Address interface. + var addr = dawg.StreetAddr{ + Street: "600 Mountain Ave bldg 5", + CityName: "New Providence", + State: "NJ", + Zipcode: "07974", + AddrType: "Business", + } + store, err := dawg.NearestStore(&addr, dawg.Delivery) + if err != nil { + log.Fatal(err) + } + fmt.Println(store.WaitTime()) +} + func ExampleNearestStore() { var addr = &dawg.StreetAddr{ Street: "1600 Pennsylvania Ave.", @@ -19,58 +50,99 @@ func ExampleNearestStore() { if err != nil { log.Fatal(err) } - fmt.Println(store.City) - fmt.Println(store.ID) // Output: // Washington - // 4336 } -func ExampleOrder_AddProduct() { - var addr = &dawg.StreetAddr{ - Street: "1600 Pennsylvania Ave.", - CityName: "Washington", - State: "DC", - Zipcode: "20500", - AddrType: "House", +func ExampleSignIn() { + user, err := dawg.SignIn(username, password) + if err != nil { + log.Fatal(err) } + fmt.Println(user.Email == username) + fmt.Printf("%T\n", user) +} - store, err := dawg.NearestStore(addr, "Delivery") +func ExampleUserProfile() { + // Obtain a dawg.UserProfile with the dawg.SignIn function + user, err := dawg.SignIn(username, password) + if err != nil { + log.Fatal(err) + } + fmt.Println(user.Email == username) + fmt.Printf("%T\n", user) +} + +func ExampleUserProfile_Cards() { + cards, err := user.Cards() + if err != nil { + log.Fatal(err) + } + fmt.Println("Test Card name:", cards[0].NickName) // This is dependant on the account +} + +func ExampleUserProfile_AddAddress() { + // The address that is stored in a dawg.UserProfile are the address that dominos + // keeps track of and an address may need to be added. + fmt.Printf("%+v\n", user.Addresses) + + user.AddAddress(&dawg.StreetAddr{ + Street: "600 Mountain Ave bldg 5", + CityName: "New Providence", + State: "NJ", + Zipcode: "07974", + AddrType: "Business", + }) + fmt.Println(len(user.Addresses) > 0) + fmt.Printf("%T\n", user.DefaultAddress()) + + // Output: + // [] + // true + // *dawg.UserAddress +} + +func ExampleOrder_AddProduct() { + store, err := dawg.NearestStore(&address, "Delivery") if err != nil { log.Fatal(err) } order := store.NewOrder() - pizza, err := store.GetProduct("16SCREEN") + pizza, err := store.GetVariant("14SCREEN") if err != nil { log.Fatal(err) } pizza.AddTopping("P", dawg.ToppingFull, "1.5") order.AddProduct(pizza) + + fmt.Println(order.Products[0].Name) + + // Output: + // Large (14") Hand Tossed Pizza } func ExampleProduct_AddTopping() { - var addr = &dawg.StreetAddr{ - Street: "1600 Pennsylvania Ave.", - CityName: "Washington", - State: "DC", - Zipcode: "20500", - AddrType: "House", - } - - store, err := dawg.NearestStore(addr, "Delivery") + store, err := dawg.NearestStore(&address, "Delivery") if err != nil { log.Fatal(err) } order := store.NewOrder() - pizza, err := store.GetProduct("16SCREEN") + pizza, err := store.GetVariant("14SCREEN") if err != nil { log.Fatal(err) } pizza.AddTopping("P", dawg.ToppingLeft, "1.0") // pepperoni on the left pizza.AddTopping("K", dawg.ToppingRight, "2.0") // double bacon on the right order.AddProduct(pizza) + + fmt.Println(pizza.Options()["P"]) + fmt.Println(pizza.Options()["K"]) + + // Output: + // map[1/2:1.0] + // map[2/2:2.0] } diff --git a/dawg/internal/auth/auth.go b/dawg/internal/auth/auth.go new file mode 100644 index 0000000..5a2bc79 --- /dev/null +++ b/dawg/internal/auth/auth.go @@ -0,0 +1,82 @@ +package auth + +import ( + "fmt" + "net/http" + "time" +) + +// NewToken returns an initialized transport. +func NewToken() *Token { + return &Token{transport: http.DefaultTransport} +} + +// Token is a JWT that can be used as a transport for an http.Client +type Token struct { + // AccessToken is the actual web token + AccessToken string `json:"access_token"` + // RefreshToken is the secret used to refresh this token + RefreshToken string `json:"refresh_token,omitempty"` + // Type is the type of token + Type string `json:"token_type"` + // ExpiresIn is the time in seconds that it takes for the token to + // expire. + ExpiresIn int `json:"expires_in"` + + transport http.RoundTripper +} + +func (t *Token) authorization() string { + return fmt.Sprintf("%s %s", t.Type, t.AccessToken) +} + +// RoundTrip implements the http.RoundTripper interface. +func (t *Token) RoundTrip(req *http.Request) (*http.Response, error) { + req.Header.Set("Authorization", t.authorization()) + SetDawgUserAgent(req.Header) + return t.transport.RoundTrip(req) +} + +func (t *Token) SetTransport(rt http.RoundTripper) { + t.transport = rt +} + +// Error is an error that is returned by the oauth endpoint. +type Error struct { + Err string `json:"error"` + ErrorDesc string `json:"error_description"` +} + +func (e *Error) Error() string { + return fmt.Sprintf("%s: %s", e.Err, e.ErrorDesc) +} + +// SetDawgUserAgent sets the package user agent +func SetDawgUserAgent(head http.Header) { + head.Set( + "User-Agent", + "Dominos API Wrapper for GO - "+time.Now().String(), + ) +} + +type doer interface { + Do(*http.Request) (*http.Response, error) +} + +var scopes = []string{ + "customer:card:read", + "customer:profile:read:extended", + "customer:orderHistory:read", + "customer:card:update", + "customer:profile:read:basic", + "customer:loyalty:read", + "customer:orderHistory:update", + "customer:card:create", + "customer:loyaltyHistory:read", + "order:place:cardOnFile", + "customer:card:delete", + "customer:orderHistory:create", + "customer:profile:update", + "easyOrder:optInOut", + "easyOrder:read", +} diff --git a/dawg/items.go b/dawg/items.go index 508ff22..0a322db 100644 --- a/dawg/items.go +++ b/dawg/items.go @@ -31,8 +31,8 @@ type Item interface { Category() string } -// item has the common fields between Product and Varient. -type item struct { +// ItemCommon has the common fields between Product and Variant. +type ItemCommon struct { Code string Name string Tags map[string]interface{} @@ -40,15 +40,16 @@ type item struct { // Local will tell you if the item was made locally Local bool - menu *Menu // not really sure how i feel about this... smells like OOP + menu *Menu // not really sure how i feel about this... smells like OOP :( } // ItemCode is a getter method for the Code field. -func (im *item) ItemCode() string { +func (im *ItemCommon) ItemCode() string { return im.Code } -func (im *item) ItemName() string { +// ItemName gives the name of the item +func (im *ItemCommon) ItemName() string { return im.Name } @@ -57,10 +58,10 @@ func (im *item) ItemName() string { // // Product is not a the most basic component of the Dominos menu; this is where // the Variant structure comes in. The Product structure can be seen as more of -// a category that houses a list of Variants. -// All exported field are initialized from json data. +// a category that houses a list of Variants. Products are still able to be ordered, +// however. type Product struct { - item + ItemCommon // Variants is the list of menu items that are a subset of this product. Variants []string @@ -127,18 +128,18 @@ func (p *Product) Category() string { return p.ProductType } -// GetVariants will initialize all the Varients the are a subset of the product. +// GetVariants will initialize all the Variants the are a subset of the product. // // The function needs a menu to get the data for each variant code. -func (p *Product) GetVariants(container ItemContainer) (varients []*Variant) { +func (p *Product) GetVariants(container ItemContainer) (variants []*Variant) { for _, code := range p.Variants { v, err := container.GetVariant(code) if err != nil { continue } - varients = append(varients, v) + variants = append(variants, v) } - return varients + return variants } func (p *Product) optionQtys() (optqtys []string) { @@ -156,7 +157,7 @@ func (p *Product) optionQtys() (optqtys []string) { // Variant is a structure that represents a base component of the Dominos menu. // It will be a subset of a Product (see Product). type Variant struct { - item + ItemCommon // the price of the variant. Price string @@ -226,7 +227,7 @@ func (v *Variant) GetProduct() *Product { return v.product } -// FindProduct will initialize the Varient with it's parent product and +// FindProduct will initialize the Variant with it's parent product and // return that products. Returns nil if product is not found. func (v *Variant) FindProduct(m *Menu) *Product { if v.product != nil { @@ -241,7 +242,7 @@ func (v *Variant) FindProduct(m *Menu) *Product { // PreConfiguredProduct is pre-configured product. type PreConfiguredProduct struct { - item + ItemCommon // Description of the product Description string `json:"Description"` @@ -267,11 +268,13 @@ func (pc *PreConfiguredProduct) Options() map[string]interface{} { // AddTopping adds a topping to the product. func (pc *PreConfiguredProduct) AddTopping(code, cover, amnt string) error { + // TODO: finish this return errors.New("not implimented") } // Category returns the product category. see Item func (pc *PreConfiguredProduct) Category() string { + // TODO: finish this return "n/a" } diff --git a/dawg/items_test.go b/dawg/items_test.go index 431f03b..45ddcfe 100644 --- a/dawg/items_test.go +++ b/dawg/items_test.go @@ -1,10 +1,22 @@ package dawg import ( + "net/http" "testing" + + "github.com/harrybrwn/apizza/pkg/tests" ) func TestProduct(t *testing.T) { + cli, mux, server := testServer() + defer server.Close() + defer swapClientWith(cli)() + mux.HandleFunc("/power/store-locator", storeLocatorHandlerFunc(t)) + mux.HandleFunc("/power/store/4344/profile", storeProfileHandlerFunc(t)) + mux.HandleFunc("/power/store/4328/menu", func(w http.ResponseWriter, r *http.Request) { + fileHandleFunc(t, "./testdata/menu.json")(w, r) + }) + menu, err := testingStore().Menu() if err != nil { t.Error(err) @@ -41,16 +53,22 @@ func TestProduct(t *testing.T) { } func TestProductToppings(t *testing.T) { + cli, mux, server := testServer() + defer server.Close() + defer swapClientWith(cli)() + mux.HandleFunc("/power/store-locator", storeLocatorHandlerFunc(t)) + mux.HandleFunc("/power/store/4344/profile", storeProfileHandlerFunc(t)) + mux.HandleFunc("/power/store/4328/menu", func(w http.ResponseWriter, r *http.Request) { + fileHandleFunc(t, "./testdata/menu.json")(w, r) + }) + + tests.InitHelpers(t) m := testingMenu() p, err := m.GetProduct("S_PIZZA") // pizza - if err != nil { - t.Fatal(err) - } + tests.Check(err) err = p.AddTopping("notatopping", ToppingFull, "1.9") - if err == nil { - t.Error("expected an error") - } + tests.Exp(err) if err.Error() != "could not make a notatopping topping" { t.Error("got the wrong error") } @@ -58,10 +76,7 @@ func TestProductToppings(t *testing.T) { if len(p.Options()) == 0 { t.Error("should not be len zero even after set to nil (see Options impl for Product)") } - err = p.AddTopping("K", ToppingLeft, "2.0") - if err != nil { - t.Error(err) - } + tests.Check(p.AddTopping("K", ToppingLeft, "2.0")) if _, ok := p.Options()["K"]; !ok { t.Error("bacon should have been added") } @@ -77,9 +92,7 @@ func TestProductToppings(t *testing.T) { } v, err := m.GetVariant("14SCREEN") - if err != nil { - t.Error(err) - } + tests.Check(err) if v.FindProduct(m) == nil { t.Error("should not be nil, pizza has a category") } @@ -98,9 +111,6 @@ func TestProductToppings(t *testing.T) { v.ProductCode = old for _, v := range m.Variants { - if v.GetProduct() != nil { - t.Error("uninitialized variant has a product already") - } if v.FindProduct(m) == nil { t.Error("should not be nil:", v.Code) } @@ -117,6 +127,14 @@ func TestProductToppings(t *testing.T) { } func TestViewOptions(t *testing.T) { + cli, mux, server := testServer() + defer server.Close() + defer swapClientWith(cli)() + mux.HandleFunc("/power/store-locator", storeLocatorHandlerFunc(t)) + mux.HandleFunc("/power/store/4344/profile", storeProfileHandlerFunc(t)) + mux.HandleFunc("/power/store/4328/menu", func(w http.ResponseWriter, r *http.Request) { + fileHandleFunc(t, "./testdata/menu.json")(w, r) + }) m := testingMenu() itm, err := m.GetVariant("P10IRECK") diff --git a/dawg/menu.go b/dawg/menu.go index 7129433..625b77e 100644 --- a/dawg/menu.go +++ b/dawg/menu.go @@ -31,10 +31,10 @@ type ItemContainer interface { // Menu represents the dominos menu. It is best if this comes from // the Store.Menu() method. type Menu struct { - ID string `json:"ID"` + ID string Categorization struct { - Food MenuCategory `json:"Food"` - Coupons MenuCategory `json:"Coupons"` + Food MenuCategory + Coupons MenuCategory Preconfigured MenuCategory `json:"PreconfiguredProducts"` } `json:"Categorization"` Products map[string]*Product @@ -42,7 +42,7 @@ type Menu struct { Toppings map[string]map[string]Topping Preconfigured map[string]*PreConfiguredProduct `json:"PreconfiguredProducts"` Sides map[string]map[string]struct { - item + ItemCommon Description string } @@ -51,7 +51,7 @@ type Menu struct { // MenuCategory is a category on the dominos menu. type MenuCategory struct { - Categories []MenuCategory `json:"Categories"` + Categories []MenuCategory Products []string Name string Code string @@ -137,13 +137,13 @@ func (m *Menu) ViewOptions(itm Item) map[string]string { // Note: this struct does not rempresent a topping that is added to an Item // and sent to dominos. type Topping struct { - item + ItemCommon Description string Availability []interface{} } -// ReadableOptions gives an Item's options in a format meant for humas. +// ReadableOptions gives an Item's options in a format meant for humans. func ReadableOptions(item Item) map[string]string { var out = map[string]string{} @@ -205,6 +205,7 @@ func makeTopping(cover, amount string, optionQtys []string) map[string]string { } if optionQtys != nil { if !validateQtys(amount, optionQtys) { + // TODO: make this return a helpful error message return nil } } @@ -213,6 +214,7 @@ func makeTopping(cover, amount string, optionQtys []string) map[string]string { case ToppingFull, ToppingLeft, ToppingRight: key = cover default: + // TODO: have this return an error message saying that the topping coverage was invalid return nil } diff --git a/dawg/menu_test.go b/dawg/menu_test.go index 5fbc77b..9615c0c 100644 --- a/dawg/menu_test.go +++ b/dawg/menu_test.go @@ -2,16 +2,29 @@ package dawg import ( "bytes" + "encoding/gob" + "net/http" + "os" + "path/filepath" "testing" + + "github.com/harrybrwn/apizza/pkg/tests" ) // Move this to an items_test.go file func TestItems(t *testing.T) { + cli, mux, server := testServer() + defer server.Close() + defer swapClientWith(cli)() + mux.HandleFunc("/power/store-locator", storeLocatorHandlerFunc(t)) + mux.HandleFunc("/power/store/4344/profile", storeProfileHandlerFunc(t)) + mux.HandleFunc("/power/store/4328/menu", func(w http.ResponseWriter, r *http.Request) { + fileHandleFunc(t, "./testdata/menu.json")(w, r) + }) + tests.InitHelpers(t) store := testingStore() menu, err := store.Menu() - if err != nil { - t.Error(err) - } + tests.Check(err) testcases := []struct { product, variant, top, cover, coverErr string @@ -50,13 +63,13 @@ func TestItems(t *testing.T) { for _, tc := range testcases { p, err := menu.GetProduct(tc.product) if tc.wanterr && err == nil { - t.Error("expected error") - } else if err != nil { - t.Error(err) + t.Errorf("expected error from menu.GetProduct(%s)", tc.product) + } else { + tests.Check(err) } v, err := menu.GetVariant(tc.variant) if tc.wanterr && err == nil { - t.Error("expected error") + t.Errorf("expected error from menu.GetVariant(%s)", tc.variant) } else if err != nil { t.Error(err) } @@ -70,12 +83,8 @@ func TestItems(t *testing.T) { t.Errorf("%s should be a variant of %s", tc.variant, tc.product) foundVariant: } - if err = p.AddTopping(tc.top, ToppingLeft, tc.cover); err != nil { - t.Error(err) - } - if err = v.AddTopping(tc.top, ToppingFull, tc.cover); err != nil { - t.Error(err) - } + tests.Check(p.AddTopping(tc.top, ToppingLeft, tc.cover)) + tests.Check(v.AddTopping(tc.top, ToppingFull, tc.cover)) if err = v.AddTopping(tc.top, "1/1", tc.coverErr); err == nil { t.Error("expected error") } @@ -86,15 +95,21 @@ func TestItems(t *testing.T) { } func TestOPFromItem(t *testing.T) { + cli, mux, server := testServer() + defer server.Close() + defer swapClientWith(cli)() + mux.HandleFunc("/power/store-locator", storeLocatorHandlerFunc(t)) + mux.HandleFunc("/power/store/4344/profile", storeProfileHandlerFunc(t)) + mux.HandleFunc("/power/store/4328/menu", func(w http.ResponseWriter, r *http.Request) { + fileHandleFunc(t, "./testdata/menu.json")(w, r) + }) + + tests.InitHelpers(t) m := testingMenu() v, err := m.GetVariant("W08PBNLW") - if err != nil { - t.Error(err) - } + tests.Check(err) p, err := m.GetProduct("S_BONELESS") - if err != nil { - t.Error(err) - } + tests.Check(err) opv := OrderProductFromItem(v) opp := OrderProductFromItem(p) @@ -118,14 +133,21 @@ func TestOPFromItem(t *testing.T) { } } - if opv.Category() != opp.Category() { - t.Error("the variant and it's parent should have the same product type") - } + tests.StrEq(opv.Category(), opp.Category(), "the variant and it's parent should have the same product type") } func TestFindItem(t *testing.T) { + cli, mux, server := testServer() + defer server.Close() + defer swapClientWith(cli)() + mux.HandleFunc("/power/store-locator", storeLocatorHandlerFunc(t)) + mux.HandleFunc("/power/store/4344/profile", storeProfileHandlerFunc(t)) + mux.HandleFunc("/power/store/4328/menu", func(w http.ResponseWriter, r *http.Request) { + fileHandleFunc(t, "./testdata/menu.json")(w, r) + }) + + tests.InitHelpers(t) m := testingMenu() - tt := []string{"W08PBNLW", "S_BONELESS", "F_PARMT", "P_14SCREEN"} for _, tc := range tt { itm := m.FindItem(tc) @@ -140,37 +162,36 @@ func TestFindItem(t *testing.T) { } _, err := m.GetProduct("nothere") - if err == nil { - t.Error("expected error") - } + tests.Exp(err) _, err = m.GetVariant("nothere") - if err == nil { - t.Error("expected error") - } + tests.Exp(err) } func TestTranslateOpt(t *testing.T) { + tests.InitHelpers(t) opts := map[string]interface{}{ "what": "no", } - if translateOpt(opts) != "what no" { - t.Error("wrong output") - } + tests.StrEq(translateOpt(opts), "what no", "wrong option translation") opt := map[string]string{ ToppingRight: "9.0", } - if translateOpt(opt) != "right 9.0" { - t.Error("wrong option translation") - } + tests.StrEq(translateOpt(opt), "right 9.0", "wrong option translation") opt = map[string]string{ ToppingLeft: "5.5", } - if translateOpt(opt) != "left 5.5" { - t.Error("wrong") - } + tests.StrEq(translateOpt(opt), "left 5.5", "wrong option translation") } func TestPrintMenu(t *testing.T) { + cli, mux, server := testServer() + defer server.Close() + defer swapClientWith(cli)() + mux.HandleFunc("/power/store-locator", storeLocatorHandlerFunc(t)) + mux.HandleFunc("/power/store/4344/profile", storeProfileHandlerFunc(t)) + mux.HandleFunc("/power/store/4328/menu", func(w http.ResponseWriter, r *http.Request) { + fileHandleFunc(t, "./testdata/menu.json")(w, r) + }) m := testingMenu() buf := new(bytes.Buffer) @@ -179,3 +200,55 @@ func TestPrintMenu(t *testing.T) { t.Error("should not have a zero length printout") } } + +func TestMenuStorage(t *testing.T) { + cli, mux, server := testServer() + defer server.Close() + defer swapClientWith(cli)() + mux.HandleFunc("/power/store-locator", storeLocatorHandlerFunc(t)) + mux.HandleFunc("/power/store/4344/profile", storeProfileHandlerFunc(t)) + mux.HandleFunc("/power/store/4328/menu", func(w http.ResponseWriter, r *http.Request) { + fileHandleFunc(t, "./testdata/menu.json")(w, r) + }) + tests.InitHelpers(t) + testdir := tests.MkTempDir(t.Name()) + + m := testingMenu() + fname := filepath.Join(testdir, "apizza-binary-menu") + buf := &bytes.Buffer{} + gob.Register([]interface{}{}) + err := gob.NewEncoder(buf).Encode(m) + tests.Fatal(err) + + f, err := os.Create(fname) + tests.Check(err) + _, err = f.Write(buf.Bytes()) + tests.Check(err) + tests.Check(f.Close()) + file, err := os.Open(fname) + tests.Check(err) + menu := Menu{} + err = gob.NewDecoder(file).Decode(&menu) + tests.Check(file.Close()) + tests.Fatal(err) + + tests.StrEq(menu.ID, m.ID, "wrong menu id") + if menu.Preconfigured == nil { + t.Fatal("should have decoded the Preconfigured products") + } + + for k := range m.Preconfigured { + mp := m.Preconfigured[k] + menup := menu.Preconfigured[k] + tests.StrEq(mp.Code, menup.Code, "Stored wrong Code - got: %s, want: %s", menup.Code, mp.Code) + tests.StrEq(mp.Opts, menup.Opts, "Stored wrong opt - got: %s, want: %s", menup.Opts, mp.Opts) + tests.StrEq(mp.Category(), menup.Category(), "Stored wrong category") + } + for k := range m.Products { + mp := m.Products[k] + menup := menu.Products[k] + tests.StrEq(mp.Code, menup.Code, "Stored wrong product code - got: %s, want: %s", menup.Code, mp.Code) + tests.StrEq(mp.DefaultSides, menup.DefaultSides, "Stored wrong product DefaultSides - got: %s, want: %s", menup.DefaultSides, mp.DefaultSides) + } + tests.Check(os.RemoveAll(testdir)) +} diff --git a/dawg/order.go b/dawg/order.go index b2a690d..97fda4e 100644 --- a/dawg/order.go +++ b/dawg/order.go @@ -7,12 +7,16 @@ import ( "fmt" ) +// TODO: alphabetize the Order struct fields and add some more documentation + // The Order struct is the main work horse of the api wrapper. The Order struct // is what will end up being sent to dominos as a json object. // -// It is suggensted that the order object be constructed from the Store.NewOrder +// It is suggested that the order object be constructed from the Store.NewOrder // method. type Order struct { + // CustomerID is a id for a customer (see UserProfile) + CustomerID string `json:",omitempty"` // LanguageCode is an ISO international language code. LanguageCode string `json:"LanguageCode"` @@ -36,14 +40,18 @@ type Order struct { } // InitOrder will make sure that an order is initialized correctly. An order -// that is not initialized correctly cannot send itslef to dominos. +// that is not initialized correctly cannot send itself to dominos. func InitOrder(o *Order) { - o.cli = orderClient + o.init() } // Init will make sure that an order is initialized correctly. An order -// that is not initialized correctly cannot send itslef to dominos. +// that is not initialized correctly cannot send itself to dominos. func (o *Order) Init() { + o.init() +} + +func (o *Order) init() { o.cli = orderClient } @@ -108,7 +116,7 @@ func (o *Order) RemoveProduct(code string) error { // AddPayment adds a payment object to an order // -// Depricated. use AddCard +// Deprecated. use AddCard func (o *Order) AddPayment(payment Payment) { p := makeOrderPaymentFromCard(&payment) o.Payments = append(o.Payments, p) @@ -116,7 +124,8 @@ func (o *Order) AddPayment(payment Payment) { // AddCard will add a card as a method of payment. func (o *Order) AddCard(c Card) { - o.Payments = append(o.Payments, makeOrderPaymentFromCard(c)) + card := makeOrderPaymentFromCard(c) + o.Payments = append(o.Payments, card) } // Name returns the name that was set by the user. @@ -132,17 +141,15 @@ func (o *Order) SetName(name string) { // Validate sends and order to the validation endpoint to be validated by // Dominos' servers. func (o *Order) Validate() error { - err := sendOrder("/power/validate-order", *o) - // TODO: make it possible to recognize the warning as an 'AutoAddedOrderId' warning. - if IsWarning(err) { - e := err.(*DominosError) - o.OrderID = e.Order.OrderID - } - return err + return ValidateOrder(o) } // only returns dominos failures or non-dominos errors. func (o *Order) prepare() error { + if o.cli == nil { + o.cli = orderClient + } + odata, err := getPricingData(*o) if err != nil && !IsWarning(err) { return err @@ -164,9 +171,12 @@ func (o *Order) prepare() error { // ValidateOrder sends and order to the validation endpoint to be validated by // Dominos' servers. func ValidateOrder(order *Order) error { + if order.cli == nil { + order.cli = orderClient + } err := sendOrder("/power/validate-order", *order) - if IsWarning(err) { - e := err.(*DominosError) + if e, ok := err.(*DominosError); ok { + // TODO: make it possible to recognize the warning as an 'AutoAddedOrderId' warning. order.OrderID = e.Order.OrderID } return err @@ -199,16 +209,16 @@ func (o *Order) raw() *bytes.Buffer { return buf } -func sendOrder(path string, ordr Order) error { - b, err := ordr.cli.post(path, nil, ordr.raw()) +func sendOrder(path string, order Order) error { + b, err := order.cli.post(path, nil, order.raw()) if err != nil { return err } return dominosErr(b) } -func orderRequest(path string, ordr *Order) (map[string]interface{}, error) { - b, err := ordr.cli.post(path, nil, ordr.raw()) +func orderRequest(path string, order *Order) (map[string]interface{}, error) { + b, err := order.cli.post(path, nil, order.raw()) respData := map[string]interface{}{} if err := errpair(err, json.Unmarshal(b, &respData)); err != nil { @@ -217,16 +227,16 @@ func orderRequest(path string, ordr *Order) (map[string]interface{}, error) { return respData, dominosErr(b) } -// does not take a pointer because ordr.Payments = nil should not be remembered -func getOrderPrice(ordr Order) (map[string]interface{}, error) { +// does not take a pointer because order.Payments = nil should not be remembered +func getOrderPrice(order Order) (map[string]interface{}, error) { // fmt.Println("deprecated... use getPricingData") - ordr.Payments = []*orderPayment{} - return orderRequest("/power/price-order", &ordr) + order.Payments = []*orderPayment{} + return orderRequest("/power/price-order", &order) } -func getPricingData(ordr Order) (*priceingData, error) { - ordr.Payments = []*orderPayment{} - b, err := ordr.cli.post("/power/price-order", nil, ordr.raw()) +func getPricingData(order Order) (*priceingData, error) { + order.Payments = []*orderPayment{} + b, err := order.cli.post("/power/price-order", nil, order.raw()) resp := &priceingData{} if err := errpair(err, json.Unmarshal(b, resp)); err != nil { return nil, err @@ -235,18 +245,20 @@ func getPricingData(ordr Order) (*priceingData, error) { } type priceingData struct { - Order struct { - OrderID string - PulseOrderGUID string `json:"PulseOrderGuid"` - Amounts map[string]float64 - AmountsBreakdown map[string]interface{} - } + Order pricedOrder +} + +type pricedOrder struct { + OrderID string + Amounts map[string]float64 + AmountsBreakdown map[string]interface{} + PulseOrderGUID string `json:"PulseOrderGuid"` } // OrderProduct represents an item that will be sent to and from dominos within // the Order struct. type OrderProduct struct { - item + ItemCommon // Qty is the number of products to be ordered. Qty int `json:"Qty"` @@ -265,7 +277,7 @@ type OrderProduct struct { // OrderProductFromItem will construct an order product from an Item. func OrderProductFromItem(itm Item) *OrderProduct { return &OrderProduct{ - item: item{ + ItemCommon: ItemCommon{ Code: itm.ItemCode(), Name: itm.ItemName(), }, @@ -285,7 +297,7 @@ func (p *OrderProduct) Category() string { return p.pType } -// ReadableOptions gives the options that are meant for humas to view. +// ReadableOptions gives the options that are meant for humans to view. func (p *OrderProduct) ReadableOptions() map[string]string { if p.menu != nil { // this menu that is passed along with item is temporary return ReadableToppings(p, p.menu) @@ -295,7 +307,7 @@ func (p *OrderProduct) ReadableOptions() map[string]string { // AddTopping adds a topping to the product. The 'code' parameter is a // topping code, a list of which can be found in the menu object. The 'coverage' -// parameter is for specifieing which side of the topping should be on for +// parameter is for specifying which side of the topping should be on for // pizza. The 'amount' parameter is 2.0, 1.5, 1.0, o.5, or 0 and gives the amount // of topping should be given. func (p *OrderProduct) AddTopping(code, coverage, amount string) error { diff --git a/dawg/order_test.go b/dawg/order_test.go index 289c7fa..e8daed5 100644 --- a/dawg/order_test.go +++ b/dawg/order_test.go @@ -4,42 +4,33 @@ import ( "encoding/json" "fmt" "io/ioutil" + "net/http" "strings" "testing" "time" + + "github.com/harrybrwn/apizza/pkg/tests" ) func TestGetOrderPrice(t *testing.T) { - o := Order{} - if o.cli == nil { - o.cli = orderClient - } - _, err := getOrderPrice(o) - if err == nil { - t.Error("Should have returned an error") - } - data, err := getPricingData(o) - if err == nil { - t.Error("should have returned an error") - } - if data == nil { - t.Error("should not have returned a nil value") - } - if len(data.Order.OrderID) == 0 { - t.Error("should alway return an order-id") - } - if !IsFailure(err) { - t.Error("this error should only be a failure") - t.Error(err.Error()) - } - + defer swapclient(10)() + // o := Order{cli: orderClient} + // _, err := getPricingData(o) + // if err == nil { + // t.Error("should have returned an error") + // } + // if !IsFailure(err) { + // t.Error("this error should only be a failure") + // t.Error(err.Error()) + // } order := Order{ cli: orderClient, LanguageCode: DefaultLang, ServiceMethod: "Delivery", - StoreID: "4336", Payments: []*orderPayment{&orderPayment{}}, OrderID: "", + StoreID: "4336", OrderID: "", + Payments: []*orderPayment{}, Products: []*OrderProduct{ - &OrderProduct{ - item: item{ + { + ItemCommon: ItemCommon{ Code: "12SCREEN", }, Opts: map[string]interface{}{ @@ -58,9 +49,6 @@ func TestGetOrderPrice(t *testing.T) { AddrType: "House", }, } - if err := ValidateOrder(&order); IsFailure(err) { - t.Error(err) - } if err := order.Validate(); IsFailure(err) { t.Error(err) } @@ -69,25 +57,22 @@ func TestGetOrderPrice(t *testing.T) { fmt.Printf("%+v\n", resp) t.Error("\n\b", e) } - if len(order.Payments) == 0 { + if len(order.Payments) != 0 { t.Fatal("order.Payments should be empty because tests were about to place an order") } - if err = order.PlaceOrder(); err == nil { - t.Error("expected error") - } order.StoreID = "" // should cause dominos to reject the order and send an error _, err = getOrderPrice(order) if err == nil { - t.Error("Should have raised an error", "\n\b", err) - } - - err = order.prepare() - if !IsFailure(err) { - t.Error("Should have returned a dominos failure", err) + t.Error("Should have raised an error", err) } + // err = order.prepare() + // if !IsFailure(err) { + // t.Error("Should have returned a dominos failure", err) + // } } func TestNewOrder(t *testing.T) { + tests.InitHelpers(t) s := testingStore() if _, err := s.GetProduct("S_PIZZA"); err != nil { t.Error(err) @@ -101,28 +86,18 @@ func TestNewOrder(t *testing.T) { t.Error("incorrect order name") } v, err := s.GetVariant("2LDCOKE") - if err != nil { - t.Fatal(err) - } - err = o.AddProductQty(v, 2) - if err != nil { - t.Error(err) - } + tests.Check(err) + tests.Check(o.AddProductQty(v, 2)) if o.Products == nil { t.Error("Products should not be empty") } pizza, err := s.GetVariant("14TMEATZA") - if err != nil { - t.Error(err) - } + tests.Check(err) if pizza == nil { t.Error("product is nil") } pizza.AddTopping("X", ToppingFull, "1.5") - err = o.AddProduct(pizza) - if err != nil { - t.Error(err) - } + tests.Check(o.AddProduct(pizza)) price, err := o.Price() if IsFailure(err) { t.Error(err) @@ -130,23 +105,16 @@ func TestNewOrder(t *testing.T) { if price == -1.0 { t.Error("Order.Price() failed") } - if err != nil { - t.Error(err) - } + tests.Check(err) } func TestPrepareOrder(t *testing.T) { + tests.InitHelpers(t) st := testingStore() o := st.MakeOrder("Bob", "Smith", "bobsmith@aol.com") - if o.FirstName != "Bob" { - t.Error("wrong first name") - } - if o.LastName != "Smith" { - t.Error("bad last name") - } - if o.Email != "bobsmith@aol.com" { - t.Error("bad email") - } + tests.StrEq(o.FirstName, "Bob", "wrong first name") + tests.StrEq(o.LastName, "Smith", "wrong last name") + tests.StrEq(o.Email, "bobsmith@aol.com", "wrong email") if o.price > 0.0 { t.Error("order should not be initialized with a price above zero") } @@ -154,17 +122,9 @@ func TestPrepareOrder(t *testing.T) { t.Error("a new order should be initialized with an order id by default") } - menu, err := st.Menu() - if err != nil { - t.Error(err) - } - if err = o.AddProduct(menu.FindItem("10SCREEN")); err != nil { - t.Error(err) - } - - if err = o.prepare(); err != nil { - t.Error("Should not have returned error:\n", err) - } + menu := testingMenu() + tests.Check(o.AddProduct(menu.FindItem("10SCREEN"))) + tests.Check(o.prepare()) if o.price <= 0.0 { t.Error("cached price should not be zero or less") } @@ -183,99 +143,115 @@ func TestPrepareOrder(t *testing.T) { } func TestOrder_Err(t *testing.T) { + tests.InitHelpers(t) addr := testAddress() addr.Street = "" - store, err := NearestStore(addr, "Delivery") - if err != nil { - t.Error(err) - } + store, err := NearestStore(addr, Delivery) + tests.Check(err) o := store.NewOrder() v, err := store.GetVariant("2LDCOKE") - if err != nil { - t.Error(err) - } + tests.Check(err) if v == nil { t.Fatal("got nil variant") } - err = o.AddProduct(v) - if err != nil { - t.Error(err) - } + tests.Check(o.AddProduct(v)) price, err := o.Price() - if err == nil { - t.Error(err) - } + tests.Exp(err) if price != -1.0 { t.Error("expected bad price") } - err = o.AddProduct(nil) - if err == nil { - t.Error("should have returned an error") - } - err = o.AddProductQty(nil, 50) - if err == nil { - t.Error("expected an error") - } + tests.Exp(o.AddProduct(nil)) + tests.Exp(o.AddProductQty(nil, 50)) o = new(Order) InitOrder(o) + tests.Exp(o.PlaceOrder()) + itm, err := store.GetVariant("12SCREEN") + tests.Check(err) + op := OrderProductFromItem(itm) + tests.Exp(op.AddTopping("test", "test", "test")) +} + +func TestRawOrder(t *testing.T) { + tests.InitHelpers(t) + var ( + err error + o *Order + reset = func() { + o = &Order{ + ServiceMethod: Delivery, + Address: &StreetAddr{}, + Email: "jake@statefarm.com", + Phone: "1234567", + } + } + ) + reset() err = o.PlaceOrder() - if err == nil { - t.Error("expected error") + if !IsFailure(err) { + t.Error("placing an empty order should fail") } - itm, err := store.GetVariant("12SCREEN") + reset() + tests.Exp(o.Validate(), "expected validation error from empty order") +} + +func TestNearestStore_WithProxy(t *testing.T) { + cli, mux, server := testServer() + defer server.Close() + defer swapClientWith(cli)() + mux.HandleFunc("/power/store-locator", storeLocatorHandlerFunc(t)) + mux.HandleFunc("/power/store/4344/profile", storeProfileHandlerFunc(t)) + store, err := NearestStore(testAddress(), Delivery) if err != nil { t.Error(err) } - op := OrderProductFromItem(itm) - err = op.AddTopping("test", "test", "test") - if err == nil { - t.Error("expected error") + if store.ID == "" { + t.Error("empty id") } } func TestRemoveProduct(t *testing.T) { - s := testingStore() - order := s.NewOrder() - menu, err := s.Menu() - if err != nil { - t.Error(err) - } + cli, mux, server := testServer() + defer server.Close() + defer swapClientWith(cli)() + mux.HandleFunc("/power/store-locator", storeLocatorHandlerFunc(t)) + mux.HandleFunc("/power/store/4344/profile", storeProfileHandlerFunc(t)) + mux.HandleFunc("/power/store/4328/menu", func(w http.ResponseWriter, r *http.Request) { + fileHandleFunc(t, "./testdata/menu.json")(w, r) + }) + + tests.InitHelpers(t) + order := &Order{ + LanguageCode: DefaultLang, + ServiceMethod: Carryout, + StoreID: "4336", + Address: testAddress(), + cli: orderClient, + } + menu := testingMenu() + productCodes := []string{"2LDCOKE", "12SCREEN", "PSANSABC", "B2PCLAVA"} for _, code := range productCodes { v, err := menu.GetVariant(code) - if err != nil { - t.Error(err) - } - order.AddProduct(v) - } - if err = order.RemoveProduct("12SCREEN"); err != nil { - t.Error(err) - } - if err = order.RemoveProduct("B2PCLAVA"); err != nil { - t.Error(err) + tests.Check(err) + tests.Check(order.AddProduct(v)) } + tests.Check(order.RemoveProduct("12SCREEN")) + tests.Check(order.RemoveProduct("B2PCLAVA")) for _, p := range order.Products { if p.Code == "12SCREEN" || p.Code == "B2PCLAVA" { t.Error("should have been removed") } } - if err = order.RemoveProduct("nothere"); err == nil { - t.Error("expected error") - } + tests.Exp(order.RemoveProduct("nothere")) } func TestOrderProduct(t *testing.T) { - s := testingStore() - menu, err := s.Menu() - if err != nil { - t.Error(err) - } + tests.InitHelpers(t) + menu := testingMenu() // this will get the menu from the same store but cached item := menu.FindItem("14SCEXTRAV") op := OrderProductFromItem(item) - if err := op.AddTopping("X", "1/1", "1"); err != nil { - t.Error(err) - } + tests.Check(op.AddTopping("X", "1/1", "1")) m := op.ReadableOptions() if len(m) <= 0 { @@ -290,10 +266,9 @@ func TestOrderProduct(t *testing.T) { } func TestCard(t *testing.T) { + tests.InitHelpers(t) c := NewCard("1234123412341234", "01/10", 111) - if c.Num() != "1234123412341234" { - t.Error("go wrong card number") - } + tests.StrEq(c.Num(), "1234123412341234", "go wrong card number") tm := c.ExpiresOn() if tm.Month() != time.January { @@ -302,12 +277,8 @@ func TestCard(t *testing.T) { if tm.Year() != 2010 { t.Error("bad expiration year:", tm.Year()) } - if c.Code() != "111" { - t.Error("bad cvv") - } - if formatDate(tm) != "0110" { - t.Error("bad date format:", formatDate(tm)) - } + tests.StrEq(c.Code(), "111", "bad cvv") + tests.StrEq(formatDate(tm), "0110", "bad date format: %s", formatDate(tm)) m, y := parseDate("01/10") if m != 1 { @@ -346,7 +317,7 @@ func TestCard(t *testing.T) { if ok { p.Expiration = "08" tm = p.ExpiresOn() - if tm != badExpiration { + if tm != BadExpiration { t.Error("a bad expiration date should have given the badExpiration variable") } } else { @@ -355,15 +326,9 @@ func TestCard(t *testing.T) { c = NewCard("0000000000000000", "08/08", 123) op := makeOrderPaymentFromCard(c) - if op.Number != c.Num() { - t.Error("bad number") - } - if op.Expiration != formatDate(c.ExpiresOn()) { - t.Error("bad expiration") - } - if op.SecurityCode != c.Code() { - t.Error("bad cvv") - } + tests.StrEq(op.Number, c.Num(), "bad number") + tests.StrEq(op.Expiration, formatDate(c.ExpiresOn()), "bad expiration") + tests.StrEq(op.SecurityCode, c.Code(), "bad cvv") } func TestOrderToJSON(t *testing.T) { @@ -396,14 +361,14 @@ func TestOrderCalls(t *testing.T) { o.Init() err := sendOrder("/power/validate-order", *o) if !IsFailure(err) || err == nil { - t.Error("expcted error") + t.Error("expected error") } o = new(Order) InitOrder(o) err = sendOrder("", *o) if err == nil { - t.Error("expcted error") + t.Error("expected error") } } diff --git a/dawg/payment.go b/dawg/payment.go index 3a5088f..13d7d5f 100644 --- a/dawg/payment.go +++ b/dawg/payment.go @@ -1,6 +1,7 @@ package dawg import ( + "errors" "fmt" "regexp" "strconv" @@ -13,7 +14,7 @@ type Card interface { // Number should return the card number. Num() string - // ExpiresOn returns the date that the payment exprires. + // ExpiresOn returns the date that the payment expires. ExpiresOn() time.Time // Code returns the security code or the cvv. @@ -60,17 +61,18 @@ func (p *Payment) Num() string { return p.Number } -var badExpiration = time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC) +// BadExpiration is a zero datetime object the is returned on error. +var BadExpiration = time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC) // ExpiresOn returns the expiration date as a time.Time. func (p *Payment) ExpiresOn() time.Time { if len(p.Expiration) == 0 { - return badExpiration + return BadExpiration } m, y := parseDate(p.Expiration) - if m < 0 || y < 0 { - return badExpiration + if m <= 0 || m > 12 || y < 0 { + return BadExpiration } return time.Date(int(y), time.Month(m), 1, 0, 0, 0, 0, time.Local) } @@ -80,6 +82,17 @@ func (p *Payment) Code() string { return p.CVV } +// ValidateCard will return an error if the card given has any bad data. +func ValidateCard(c Card) error { + if BadExpiration.Equal(c.ExpiresOn()) { + return errors.New("card has a bad expiration date format") + } + if findCardType(c.Num()) == "" { + return errors.New("could not find card ") + } + return nil +} + var _ Card = (*Payment)(nil) func makeOrderPaymentFromCard(c Card) *orderPayment { @@ -100,8 +113,15 @@ func formatDate(t time.Time) string { return fmt.Sprintf("%02d%s", t.Month(), year) } +// in the future, i may use `time.Parse("2/06", dateString)` +// and then try that with a few different date formats like "2/2006" or "02-06" func parseDate(d string) (month int, year int) { parts := strings.Split(d, "/") + + if len(parts) == 1 && len(d) == 4 { + // we have been given mmYY instead of mm/YY + parts = []string{d[:2], d[2:]} + } if len(parts) != 2 { return -1, -1 } @@ -153,7 +173,8 @@ type orderPayment struct { // These next fields are just for dominos Amount float64 + CardID string `json:"CardID,omitempty"` ProviderID string OTP string - GpmPaymentType string `json:"gpmPaymentType"` + GpmPaymentType string `json:"gpmPaymentType,omitempty"` } diff --git a/dawg/store.go b/dawg/store.go index 7fd2916..aec0e02 100644 --- a/dawg/store.go +++ b/dawg/store.go @@ -7,6 +7,8 @@ import ( "net/http" "sync" "time" + + "github.com/harrybrwn/apizza/dawg/internal/auth" ) const ( @@ -17,16 +19,19 @@ const ( // Carryout is a dominos service method that // will require users to go and pickup their pizza. Carryout = "Carryout" -) -// ErrBadService is returned if a service is needed but the service validation failed. -var ErrBadService = errors.New("service must be either 'Delivery' or 'Carryout'") + // DefaultLang is the package language variable + DefaultLang = "en" + + profileEndpoint = "/power/store/%s/profile" + orderHost = "order.dominos.com" +) // NearestStore gets the dominos location closest to the given address. // // The addr argument should be the address to deliver to not the address of the // store itself. The service should be either "Carryout" or "Delivery", this will -// deturmine wether the final order will be for pickup or delivery. +// determine wether the final order will be for pickup or delivery. func NearestStore(addr Address, service string) (*Store, error) { return getNearestStore(orderClient, addr, service) } @@ -55,14 +60,12 @@ func NewStore(id string, service string, addr Address) (*Store, error) { // The obj argument is anything that would be used to decode json data. // // Use type '*map[string]interface{}' in the object argument for all the -// store data +// store data. +// store := map[string]interface{}{} +// err := dawg.InitStore(id, &store) +// This will allow all of the fields sent in the api to be viewed. func InitStore(id string, obj interface{}) error { - path := fmt.Sprintf("/power/store/%s/profile", id) - b, err := orderClient.get(path, nil) - if err != nil { - return err - } - return errpair(json.Unmarshal(b, obj), dominosErr(b)) + return initStore(orderClient, id, obj) } var orderClient = &client{ @@ -70,75 +73,72 @@ var orderClient = &client{ Client: &http.Client{ Timeout: 60 * time.Second, CheckRedirect: noRedirects, + Transport: newRoundTripper(func(req *http.Request) error { + auth.SetDawgUserAgent(req.Header) + return nil + }), }, } -func initStore(cli *client, id string, store *Store) error { - path := fmt.Sprintf("/power/store/%s/profile", id) +func initStore(cli *client, id string, obj interface{}) error { + path := fmt.Sprintf(profileEndpoint, id) b, err := cli.get(path, nil) if err != nil { return err } - store.cli = cli - return errpair(json.Unmarshal(b, store), dominosErr(b)) -} - -type storebuilder struct { - sync.WaitGroup - stores chan maybeStore -} - -type maybeStore struct { - store *Store - index int - err error + if store, ok := obj.(*Store); ok { + store.cli = cli + } + return errpair(json.Unmarshal(b, obj), dominosErr(b)) } -func (sb *storebuilder) initStore(cli *client, id string, index int) { - defer sb.Done() - path := fmt.Sprintf("/power/store/%s/profile", id) - store := &Store{} +// The Store object represents a physical dominos location. +type Store struct { + ID string `json:"StoreID"` - b, err := cli.get(path, nil) - if err != nil { - sb.stores <- maybeStore{store: nil, err: err, index: -1} - } + IsOpen bool + IsOnlineNow bool + IsDeliveryStore bool + Phone string - err = errpair(json.Unmarshal(b, store), dominosErr(b)) - if err != nil { - sb.stores <- maybeStore{store: nil, err: err, index: -1} - } + PaymentTypes []string `json:"AcceptablePaymentTypes"` + CreditCardTypes []string `json:"AcceptableCreditCards"` - sb.stores <- maybeStore{store: store, err: nil, index: index} -} + Address string `json:"AddressDescription"` + PostalCode string + City string + StreetName string + StoreCoords map[string]string `json:"StoreCoordinates"` + // Min and Max distance + MinDistance, MaxDistance float64 -// The Store object represents a physical dominos location. -type Store struct { - ID string `json:"StoreID"` - Status int `json:"Status"` - IsOpen bool `json:"IsOpen"` - IsOnlineNow bool `json:"IsOnlineNow"` - IsDeliveryStore bool `json:"IsDeliveryStore"` - Phone string `json:"Phone"` - PaymentTypes []string `json:"AcceptablePaymentTypes"` - CreditCardTypes []string `json:"AcceptableCreditCards"` - MinDistance float64 `json:"MinDistance"` - MaxDistance float64 `json:"MaxDistance"` - Address string `json:"AddressDescription"` - PostalCode string `json:"PostalCode"` - City string `json:"City"` - StoreCoords map[string]string `json:"StoreCoordinates"` + ServiceIsOpen map[string]bool ServiceEstimatedWait map[string]struct { - Min int `json:"Min"` - Max int `json:"Max"` + Min, Max int } `json:"ServiceMethodEstimatedWaitMinutes"` - Hours map[string][]map[string]string `json:"Hours"` - MinDeliveryOrderAmnt float64 `json:"MinimumDeliveryOrderAmount"` - userAddress Address - userService string + AllowCarryoutOrders, AllowDeliveryOrders bool + + // Hours describes when the store will be open + Hours StoreHours + // ServiceHours describes when the store supports a given service + ServiceHours map[string]StoreHours + + MinDeliveryOrderAmnt float64 `json:"MinimumDeliveryOrderAmount"` + + Status int - menu *Menu - cli *client + userAddress Address + userService string + menu *Menu + cli *client +} + +// StoreHours is a struct that holds Dominos store hours. +type StoreHours struct { + Sun, Mon, Tue, Wed, Thu, Fri, Sat []struct { + OpenTime string + CloseTime string + } } // Menu returns the menu for a store object @@ -198,6 +198,15 @@ func (s *Store) GetVariant(code string) (*Variant, error) { return menu.GetVariant(code) } +// FindItem is a helper function for Menu.FindItem +func (s *Store) FindItem(code string) (Item, error) { + menu, err := s.Menu() + if err != nil { + return nil, err + } + return menu.FindItem(code), nil +} + // WaitTime returns a pair of integers that are the maximum and // minimum estimated wait time for that store. func (s *Store) WaitTime() (min int, max int) { @@ -205,7 +214,9 @@ func (s *Store) WaitTime() (min int, max int) { return m[s.userService].Min, m[s.userService].Max } -type storeLocs struct { +// StoreLocs is an internal struct and should not be used. +// This is only exported because of json Unmarshalling. +type StoreLocs struct { Granularity string `json:"Granularity"` Address *StreetAddr `json:"Address"` Stores []*Store `json:"Stores"` @@ -231,14 +242,13 @@ func getNearestStore(c *client, addr Address, service string) (*Store, error) { return store, initStore(c, store.ID, store) } -func findNearbyStores(c *client, addr Address, service string) (*storeLocs, error) { +func findNearbyStores(c *client, addr Address, service string) (*StoreLocs, error) { if !(service == Delivery || service == Carryout) { - // panic("service must be either 'Delivery' or 'Carryout'") return nil, ErrBadService } // TODO: on the dominos website, the c param can sometimes be just the zip code // and it still works. - b, err := c.get("/power/store-locator", &Params{ + resp, err := get(c, orderHost, "/power/store-locator", &Params{ "s": addr.LineOne(), "c": format("%s, %s %s", addr.City(), addr.StateCode(), addr.Zip()), "type": service, @@ -246,25 +256,30 @@ func findNearbyStores(c *client, addr Address, service string) (*storeLocs, erro if err != nil { return nil, err } - locs := &storeLocs{} - err = json.Unmarshal(b, locs) - if err != nil { + defer resp.Body.Close() + + result := struct { + *StoreLocs + *DominosError + }{nil, nil} + if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } - return locs, dominosErr(b) + if result.DominosError.Status != OkStatus { + return nil, result.DominosError + } + return result.StoreLocs, nil } func asyncNearbyStores(cli *client, addr Address, service string) ([]*Store, error) { all, err := findNearbyStores(cli, addr, service) if err != nil { - return nil, err + return nil, fmt.Errorf("findNearbyStores: %v", err) } var ( - nstores = len(all.Stores) - stores = make([]*Store, nstores) // return value - - i int + nStores = len(all.Stores) + stores = make([]*Store, nStores) // return value store *Store pair maybeStore builder = storebuilder{ @@ -272,11 +287,11 @@ func asyncNearbyStores(cli *client, addr Address, service string) ([]*Store, err stores: make(chan maybeStore), } ) - builder.Add(nstores) + builder.Add(nStores) go func() { defer close(builder.stores) - for i, store = range all.Stores { + for i, store := range all.Stores { go builder.initStore(cli, store.ID, i) } @@ -295,5 +310,34 @@ func asyncNearbyStores(cli *client, addr Address, service string) ([]*Store, err stores[pair.index] = store } - return stores, nil + return stores, err +} + +type storebuilder struct { + sync.WaitGroup + stores chan maybeStore +} + +type maybeStore struct { + store *Store + index int + err error +} + +func (sb *storebuilder) initStore(cli *client, id string, index int) { + defer sb.Done() + path := fmt.Sprintf(profileEndpoint, id) + store := &Store{} + + b, err := cli.get(path, nil) + if err != nil { + sb.stores <- maybeStore{store: nil, err: err, index: -1} + } + + err = errpair(json.Unmarshal(b, store), dominosErr(b)) + if err != nil { + sb.stores <- maybeStore{store: nil, err: err, index: -1} + } + + sb.stores <- maybeStore{store: store, err: nil, index: index} } diff --git a/dawg/store_test.go b/dawg/store_test.go index 78d3a2c..d2fa7fe 100644 --- a/dawg/store_test.go +++ b/dawg/store_test.go @@ -3,6 +3,8 @@ package dawg import ( "fmt" "testing" + + "github.com/harrybrwn/apizza/pkg/tests" ) func testAddress() *StreetAddr { @@ -15,32 +17,6 @@ func testAddress() *StreetAddr { } } -func TestFindNearbyStores(t *testing.T) { - for _, service := range []string{Delivery, Carryout} { - _, err := findNearbyStores(orderClient, testAddress(), service) - if err != nil { - t.Error("\n TestNearbyStore:", err, "\n") - } - } -} - -func TestFindNearbyStores_Err(t *testing.T) { - _, err := findNearbyStores(orderClient, testAddress(), "invalid service") - if err == nil { - t.Error("expected an error") - } - if err != ErrBadService { - t.Error("findNearbyStores gave the wrong error for an invalid service") - } - _, err = findNearbyStores(orderClient, &StreetAddr{}, Delivery) - if err == nil { - t.Error("should return error") - } - if _, err := NearestStore(nil, Delivery); err == nil { - t.Error("expected error") - } -} - func TestNewStore(t *testing.T) { id := "4339" service := Carryout @@ -64,169 +40,92 @@ func TestNewStore(t *testing.T) { } } -func TestNearestStore(t *testing.T) { - addr := testAddress() - service := Delivery - s, err := NearestStore(addr, service) - if err != nil { - t.Error(err) - } - if s.cli == nil { - t.Error("NearestStore should not return a store with a nil client") - } - if s.userService != service { - t.Error("userService was changed in NearestStore") - } - nearbyStores, err := findNearbyStores(orderClient, addr, service) - if err != nil { - t.Error(err) - } - if nearbyStores.Stores[0].ID != s.ID { - t.Error("NearestStore and findNearbyStores found different stores for the same address") - } - for _, s := range nearbyStores.Stores { - if s.userAddress != nil { - t.Error("userAddress should not have been initialized") - continue - } - if s.userService != "" { - t.Error("the userService should not have been initialized") - continue - } - } - m, err1 := s.Menu() - m2, err2 := s.Menu() - err = errpair(err1, err2) - if err != nil { - t.Error(err) - } - if m != m2 { - t.Errorf("should be the same for two calls to Store.Menu: %p %p", m, m2) - } - id := s.ID - s.ID = "" - _, err = s.Menu() - if err == nil { - t.Error("expected an error for a store with no id") - } - _, err = s.GetProduct("14SCREEN") - if err == nil { - t.Error("expected error from a store with no id") - } - _, err = s.GetVariant("14SCREEN") - if err == nil { - t.Error("expected error from a store with no id") - } - s.ID = id -} - -func TestGetAllNearbyStores_Async(t *testing.T) { - addr := testAddress() - for _, service := range []string{Delivery, Carryout} { - stores, err := GetNearbyStores(addr, service) - if err != nil { - t.Error(err) - } - for _, s := range stores { - if s == nil { - t.Error("should not have nil store") - } - if s.userAddress == nil { - t.Fatal("nil store.userAddress") - } - if s.userService != service { - t.Error("wrong service method") - } - if s.userAddress.City() != addr.City() { - t.Error("wrong city") - } - if s.userAddress.LineOne() != addr.LineOne() { - t.Error("wrong line one") - } - if s.userAddress.StateCode() != addr.StateCode() { - t.Error("wrong state code") - } - if s.userAddress.Zip() != addr.Zip() { - t.Error("wrong zip co de") - } - } - } -} - -func TestGetAllNearbyStores_Async_Err(t *testing.T) { - _, err := GetNearbyStores(&StreetAddr{}, Delivery) - if err == nil { - t.Error("expected error") - } - _, err = GetNearbyStores(testAddress(), "") - if err == nil { - t.Error("expected error") - } -} - func TestNearestStore_Err(t *testing.T) { _, err := NearestStore(&StreetAddr{}, Delivery) if err == nil { t.Error("expected error") } + e, ok := err.(*DominosError) + if !ok { + t.Error("should return dominos error") + } + if e.Status != -1 { + t.Error("should be a dominos failure") + } } func TestGetAllNearbyStores(t *testing.T) { - validationStores, err := findNearbyStores(orderClient, testAddress(), "Delivery") - if err != nil { - t.Error(err) - } - stores, err := GetNearbyStores(testAddress(), "Delivery") + tests.InitHelpers(t) + addr := testAddress() + validation, err := findNearbyStores(orderClient, addr, "Delivery") if err != nil { t.Error(err) } + stores, err := GetNearbyStores(addr, Delivery) + tests.Check(err) for i, s := range stores { - if s.ID != validationStores.Stores[i].ID { - t.Error("ids are not the same", stores[i].ID, validationStores.Stores[i].ID) + if s == nil { + t.Error("should not have nil store") + } + if s.userAddress == nil { + t.Fatal("nil store.userAddress") } if s.cli == nil { t.Error("should not have nil client when returned from GetNearbyStores") } - } - _, err = GetNearbyStores(&StreetAddr{}, "Delivery") - if err == nil { - t.Error("expected error") - } + tests.StrEq(s.ID, validation.Stores[i].ID, "ids are not the same %s %s", stores[i].ID, validation.Stores[i].ID) + tests.StrEq(s.Phone, validation.Stores[i].Phone, "wrong phone") + tests.StrEq(s.userService, Delivery, "wrong service method") + tests.StrEq(s.userAddress.City(), addr.City(), "wrong city") + tests.StrEq(s.userAddress.LineOne(), addr.LineOne(), "wrong line one") + tests.StrEq(s.userAddress.StateCode(), addr.StateCode(), "wrong state code") + tests.StrEq(s.userAddress.Zip(), addr.Zip(), "wrong zip code") + } + _, err = GetNearbyStores(&StreetAddr{}, Delivery) + tests.Exp(err) + _, err = GetNearbyStores(testAddress(), "") + tests.Exp(err) } func TestInitStore(t *testing.T) { - id := "4336" + tests.InitHelpers(t) m := map[string]interface{}{} - if err := InitStore(id, &m); err != nil { - t.Error(err) + check := func(k string, exp interface{}) { + if res, ok := m[k]; !ok { + t.Errorf("key: %s not in store map\n", k) + } else if res != exp { + t.Errorf("store map: got %s; want %s\n", res, exp) + } } - if m["StoreID"] != id { - t.Error("wrong store id") + id := "4336" + tests.Check(InitStore(id, &m)) + check("StoreID", id) + check("City", "Washington") + check("Region", "DC") + check("PreferredCurrency", "USD") + check("PostalCode", "20005") + ks := []string{"AddressDescription", "Phone", "AcceptablePaymentTypes", "IsOpen", "IsOnlineNow"} + for _, k := range ks { + if _, ok := m[k]; !ok { + t.Errorf("did not find key %s\n", k) + } } + test := &struct { Status int StoreID string }{} - if err := InitStore(id, test); err != nil { - t.Error(err) - } - if test.StoreID != id { - t.Error("error in InitStore for custom struct") - } + tests.Check(InitStore(id, test)) + tests.StrEq(test.StoreID, id, "error in InitStore for custom struct") if test.Status != 0 { t.Error("bad status") } sMap := map[string]interface{}{} - err := InitStore("", &sMap) - if err == nil { - t.Error("expected error") - } - if err = InitStore("1234", &sMap); err == nil { - t.Error("expected error") - } + tests.Exp(InitStore("", &sMap)) + tests.Exp(InitStore("1234", &sMap)) } -func Test_initStoreErr(t *testing.T) { +func TestInitStore_Err(t *testing.T) { ids := []string{"", "0000", "999999999999", "-7765"} for _, id := range ids { s := new(Store) @@ -249,32 +148,3 @@ func TestGetNearestStore(t *testing.T) { } } } - -func TestAsyncInOrder(t *testing.T) { - addr := testAddress() - serv := Delivery - storesInOrder, err := GetNearbyStores(addr, serv) - if err != nil { - t.Error(err) - } - stores, err := asyncNearbyStores(orderClient, addr, serv) - if err != nil { - t.Error(err) - } - - n := len(storesInOrder) - if n != len(stores) { - t.Fatal("the results did not return lists of the same length") - } - for i := 0; i < n; i++ { - if storesInOrder[i].ID != stores[i].ID { - t.Error("wrong id") - } - if storesInOrder[i].Phone != stores[i].Phone { - t.Error("stores have different phone numbers") - } - if storesInOrder[i].Address != stores[i].Address { - t.Error("stores have different addresses") - } - } -} diff --git a/dawg/testdata/menu.json b/dawg/testdata/menu.json new file mode 100644 index 0000000..bfe7858 --- /dev/null +++ b/dawg/testdata/menu.json @@ -0,0 +1 @@ +{"Misc":{"Status":0,"StoreID":"4336","BusinessDate":"2020-06-05","StoreAsOfTime":"2020-06-05 09:50:28","LanguageCode":"en","Version":"1.001","ExpiresOn":""},"Categorization":{"Food":{"Categories":[{"Categories":[{"Categories":[],"Code":"BuildYourOwn","Description":"","Products":["S_PIZZA"],"Name":"Build Your Own","Tags":{}},{"Categories":[],"Code":"Artisan","Description":"","Products":[],"Name":"","Tags":{}},{"Categories":[],"Code":"Specialty","Description":"","Products":["S_ZZ","S_MX","S_PIZPH","S_PIZPV","S_PIZUH","S_DX","S_PIZCR","S_PIZBP","S_PIZPX","S_PIZCK","S_PIZCZ","S_PISPF"],"Name":"Specialty Pizzas","Tags":{}}],"Code":"Pizza","Description":"","Products":[],"Name":"Pizza","Tags":{"PartCount":"2","OptionQtys":"0:0.5:1:1.5:2","MaxOptionQty":"10","DefaultSpecialtyCode":"PIZZA","PageTags":"Specialty","NeedsCustomization":true,"CouponTier":"MultiplePizza:MultiplePizzaB:MultiplePizzaC:MultiplePizzaD","IsDisplayedOnMakeline":true}},{"Categories":[{"Categories":[],"Code":"Slice","Description":"","Products":[],"Name":"Domino's Sandwich Slice™","Tags":{}},{"Categories":[],"Code":"Sandwich","Description":"","Products":["S_BUFC","S_CHHB","S_MEDV","S_PHIL","S_CHIKK","S_ITAL","S_CHIKP"],"Name":"Sandwiches","Tags":{"OptionQtys":"0:0.5:1:1.5","MaxOptionQty":"9","MaxSauceQty":"2","IsDisplayedOnMakeline":true}},{"Categories":[],"Code":"Hoagie","Description":"","Products":[],"Name":"Hoagies","Tags":{}}],"Code":"Sandwich","Description":"","Products":[],"Name":"Sandwiches","Tags":{"OptionQtys":"0:0.5:1:1.5","MaxOptionQty":"9","MaxSauceQty":"2","IsDisplayedOnMakeline":true}},{"Categories":[],"Code":"Pasta","Description":"","Products":["S_ALFR","S_MARIN","S_CARB","S_PRIM","S_BUILD"],"Name":"Pasta","Tags":{"OptionQtys":"0:1","MaxOptionQty":"3","IsDisplayedOnMakeline":true}},{"Categories":[],"Code":"Wings","Description":"","Products":["S_SCCBT","S_SCCHB","S_SCSJP","S_SCSBBQ","S_HOTWINGS","S_BBQW","S_PLNWINGS","S_SMANG","S_BONELESS"],"Name":"Chicken","Tags":{"OptionQtys":"0:0.5:1:1.5:2:3:4:5","MaxOptionQty":"99","IsDisplayedOnMakeline":true}},{"Categories":[],"Code":"Bread","Description":"","Products":["F_PARMT","F_GARLICT","F_SCBRD","F_SSBRD","F_SBBRD","F_PBITES"],"Name":"Breads","Tags":{"AdditionalProducts":"B8PCCT","OptionQtys":"0:0.5:1","MaxOptionQty":"99","IsDisplayedOnMakeline":true}},{"Categories":[],"Code":"GSalad","Description":"","Products":["F_GARDEN","F_CCAESAR"],"Name":"Salads","Tags":{"MaxOptionQty":"99"}},{"Categories":[],"Code":"Chips","Description":"","Products":[],"Name":"","Tags":{}},{"Categories":[],"Code":"Drinks","Description":"","Products":["F_COKE","F_DIET","F_SPRITE","F_WATER","F_ORAN","F_FITLEM"],"Name":"Drinks","Tags":{}},{"Categories":[],"Code":"Dessert","Description":"","Products":["F_CINNAT","F_MRBRWNE","F_LAVA"],"Name":"Desserts","Tags":{"MaxOptionQty":"99","IsDisplayedOnMakeline":true}},{"Categories":[],"Code":"Sides","Description":"","Products":["F_SIDJAL","F_SIDPAR","F_SIDRED","F_HOTCUP","F_SMHAB","F_BBQC","F_SIDRAN","F_Bd","F_SIDGAR","F_SIDICE","F_SIDMAR","F_CAESAR","F_ITAL","F_RANCHPK","F_STJUDE","F_BALVIN","F__SCHOOL"],"Name":"Extras","Tags":{}}],"Code":"Food","Description":"Food Items","Products":[],"Name":"Food","Tags":{}},"Coupons":{"Categories":[{"Categories":[],"Code":"Feeds1To2","Description":"","Products":["9174","2013","8382","9193","9152"],"Name":"Feeds 1-2","Tags":{}},{"Categories":[],"Code":"Feeds3To5","Description":"","Products":["5385","5918","9174","5916","9171","2013","9003","4337","9193"],"Name":"Feeds 3-5","Tags":{}},{"Categories":[],"Code":"Feeds6Plus","Description":"","Products":["2013","9193"],"Name":"Feeds 6+","Tags":{}},{"Categories":[],"Code":"LunchOffers","Description":"","Products":[],"Name":"","Tags":{}},{"Categories":[],"Code":"All","Description":"","Products":["5933","4342","5385","5918","9174","0512","5916","9171","2013","9003","4337","8211","8149","9204","8382","9193","9152"],"Name":"See All","Tags":{}},{"Categories":[],"Code":"AllStoreCoupons","Description":"","Products":["5933","4342","5385","5918","9174","0512","5916","9171","2013","9003","4337","8211","8149","9204","8382","9193","9152"],"Name":"All Available Coupons","Tags":{}}],"Code":"Coupons","Description":"Coupon Items","Products":[],"Name":"Coupons","Tags":{}},"PreconfiguredProducts":{"Categories":[{"Categories":[{"Categories":[],"Code":"GroupOrderingPizza","Description":"Pizza","Products":["14SCREEN","P_14SCREEN","S_14SCREEN","PS_14SCREEN","PM_14SCREEN","P12IPAZA","P_P12IPAZA","P_P10IGFZA","14SCEXTRAV","P14ITHPV"],"Name":"Pizza","Tags":{}},{"Categories":[],"Code":"GroupOrderingChicken","Description":"Chicken","Products":["W40PBNLW","W14PBNLW","W40PHOTW","W14PHOTW","W40PBBQW","W14PBBQW"],"Name":"Chicken","Tags":{}},{"Categories":[],"Code":"GroupOrderingBread","Description":"Breads","Products":["B32PBIT","B8PCSCB"],"Name":"Breads","Tags":{}},{"Categories":[],"Code":"GroupOrderingDessert","Description":"Desserts","Products":["MARBRWNE"],"Name":"Desserts","Tags":{}},{"Categories":[],"Code":"GroupOrderingDrink","Description":"Drinks","Products":["2LCOKE","2LDCOKE","2LSPRITE"],"Name":"Drinks","Tags":{}}],"Code":"GroupOrdering","Description":"Group Ordering","Products":[],"Name":"Group Ordering","Tags":{}},{"Categories":[{"Categories":[],"Code":"PopularItemsPizza","Description":"Pizza","Products":["XC_14SCREEN","PXC_14SCREEN","MPXC_12SCREEN","XCFeCsCpRMORrSiTd_P12IREPV"],"Name":"Pizza","Tags":{}},{"Categories":[],"Code":"PopularItemsSandwichesSidesDesserts","Description":"Sandwiches Sides Desserts","Products":["RdCKDuPv_PSANSACB","XfDu_PINPASCA","SIDRAN_W08PBBQW","B2PCLAVA"],"Name":"Sandwiches Sides Desserts","Tags":{}}],"Code":"PopularItems","Description":"Popular Items","Products":[],"Name":"Popular Items","Tags":{}}],"Code":"PreconfiguredProducts","Description":"Preconfigured Products","Products":[],"Name":"Preconfigured Products","Tags":{}}},"Coupons":{"0512":{"Code":"0512","ImageCode":"L3T,MCB","Description":"","Name":"Marble Cookie Brownie","Price":"6.99","Tags":{"ValidServiceMethods":["Carryout","Delivery","Hotspot"],"Combine":"Complementary"},"Local":true,"Bundle":true},"2013":{"Code":"2013","ImageCode":"2T-GFC","Description":"","Name":"Small Gluten Free Crust Pizza with up to 3 Toppings","Price":"9.99","Tags":{"ValidServiceMethods":["Carryout","Delivery","Hotspot"],"EffectiveOn":"2013-09-24","MultiSame":true,"Combine":"Complementary"},"Local":true,"Bundle":false},"4337":{"Code":"4337","ImageCode":"2M3T,SCB,2L","Description":"","Name":"2 Medium 3 Topping Pizzas, Stuffed Cheesy Bread & a 2 Liter","Price":"24.99","Tags":{"ValidServiceMethods":["Carryout","Delivery","Hotspot"]},"Local":true,"Bundle":true},"4342":{"Code":"4342","ImageCode":"LSO_L3T,M1T","Description":"","Name":"2 or more Medium 3-Topping Pizzas. Each Priced At:","Price":"8.49","Tags":{"ValidServiceMethods":["Carryout","Delivery","Hotspot"],"LSO_L3T":true,"M1T":true},"Local":true,"Bundle":false},"5385":{"Code":"5385","ImageCode":"2L2T","Description":"","Name":"2 or More Large 2 Topping Pizzas. Each Priced At:","Price":"11.49","Tags":{"ValidServiceMethods":["Carryout","Delivery","Hotspot"],"EffectiveOn":"2012-02-22","MultiSame":true},"Local":true,"Bundle":false},"5916":{"Code":"5916","ImageCode":"L2T,Salad","Description":"","Name":"Large 2-topping pizza and a Salad","Price":"18.99","Tags":{"ValidServiceMethods":["Carryout","Delivery","Hotspot"]},"Local":true,"Bundle":true},"5918":{"Code":"5918","ImageCode":"1LSpec,PBB,2LTR","Description":"","Name":"Large Specialty Pizza, 16 pc Parmesan bread bites and a 2 liter","Price":"21.99","Tags":{"ValidServiceMethods":["Carryout","Delivery","Hotspot"]},"Local":true,"Bundle":true},"5933":{"Code":"5933","ImageCode":"L3T,BT,2L","Description":"","Name":"Large 3 Topping Pizza, 8-piece Bread Twists and 2 Liter of Coke.","Price":"19.99","Tags":{"ValidServiceMethods":["Carryout","Delivery","Hotspot"]},"Local":true,"Bundle":true},"8149":{"Code":"8149","ImageCode":"TWIST","Description":"","Name":"One 8 piece order of Bread Twists","Price":"6.99","Tags":{"ValidServiceMethods":["Carryout","Delivery","Hotspot"],"Combine":"Complementary"},"Local":true,"Bundle":true},"8211":{"Code":"8211","ImageCode":"LTR","Description":"","Name":"2 Liter of Coca-Cola®","Price":"2.99","Tags":{"ValidServiceMethods":["Carryout","Delivery","Hotspot"]},"Local":true,"Bundle":true},"8382":{"Code":"8382","ImageCode":"1SW","Description":"","Name":"Any Oven Baked Sandwich","Price":"6.99","Tags":{"ValidServiceMethods":["Carryout","Delivery","Hotspot"],"Combine":"Complementary","EffectiveOn":"2008-05-26"},"Local":true,"Bundle":true},"9003":{"Code":"9003","ImageCode":"2L1T,LTR","Description":"","Name":"2 Large 1 Topping Pizzas and a 2-Liter of Coca-Cola®","Price":"19.99","Tags":{"ValidServiceMethods":["Carryout","Delivery","Hotspot"]},"Local":true,"Bundle":true},"9152":{"Code":"9152","ImageCode":"LC","Description":"","Name":"A 2-Piece order of Chocolate Lava Crunch Cakes","Price":"4.99","Tags":{"EffectiveOn":"2009-12-21","ValidServiceMethods":["Carryout","Delivery","Hotspot"],"Combine":"Complementary"},"Local":true,"Bundle":true},"9171":{"Code":"9171","ImageCode":"1L1T,C","Description":"","Name":"Large 1-Topping Pizza & a 8-piece order of Chicken","Price":"18.99","Tags":{"ValidServiceMethods":["Carryout","Delivery","Hotspot"],"EffectiveOn":"2011-02-21"},"Local":true,"Bundle":true},"9174":{"Code":"9174","ImageCode":"AUG19WLC","Description":"","Name":"Any of our 5 crusts, up to 3 toppings. Excludes XL & Specialty Pizzas. Crust availability varies by size for $7.99 Each. Carryout only.","Price":"7.99","Tags":{"ServiceMethods":"Carryout","ValidServiceMethods":"Carryout","MultiSame":true,"combinedSizeAndCrust":true},"Local":false,"Bundle":false},"9193":{"Code":"9193","ImageCode":"599mixmatch","Description":"","Name":"Choose any 2 or more; Medium 2-Topping Pizza, Bread Twists, Salad, Marbled Cookie Brownie, Specialty Chicken, Oven Baked Sandwich, Stuffed Cheesy Bread, 8-Piece Boneless Chicken, or Pasta in a Dish for $5.99 each.","Price":"","Tags":{"ValidServiceMethods":["Carryout","Delivery","Hotspot"],"EffectiveOn":"2013-01-03","NoPulseDefaults":true,"WizardUpsells":["{'Pizza'","{'TitleText'","'Looking for Specialty Pizza?','LinkText'","'Upgrade for only $2 more','CouponCode'","'8682'}}"]},"Local":false,"Bundle":false},"9204":{"Code":"9204","ImageCode":"M2TPAN","Description":"","Name":"Medium 2-Topping Handmade Pan Pizzas","Price":"8.99","Tags":{"EffectiveOn":"2016-12-01","ValidServiceMethods":["Carryout","Delivery","Hotspot"],"MultiSame":true},"Local":false,"Bundle":false}},"Flavors":{"Pasta":{"PASTA":{"Code":"PASTA","Description":"Pasta served in a dish.","Local":false,"Name":"Dish","SortSeq":"01"},"BBOWL":{"Code":"BBOWL","Description":"Pasta served in a bread bowl and then baked to perfection.","Local":false,"Name":"BreadBowl","SortSeq":"02"}},"Pizza":{"HANDTOSS":{"Code":"HANDTOSS","Description":"Garlic-seasoned crust with a rich, buttery taste.","Local":false,"Name":"Hand Tossed","SortSeq":"01"},"NPAN":{"Code":"NPAN","Description":"Two layers of cheese, toppings to the edge, baked in a pan for a crust that is golden and crispy with a buttery taste.","Local":false,"Name":"Handmade Pan","SortSeq":"02"},"THIN":{"Code":"THIN","Description":"Thin enough for the optimum crispy to crunchy ratio and square cut to be perfectly sharable.","Local":false,"Name":"Crunchy Thin Crust","SortSeq":"03"},"BK":{"Code":"BK","Description":"Hand stretched to be big, thin and perfectly foldable.","Local":false,"Name":"Brooklyn Style","SortSeq":"06"},"GLUTENF":{"Code":"GLUTENF","Description":"Domino's pizza made with a Gluten Free Crust.","Local":false,"Name":"Gluten Free Crust","SortSeq":"09"}},"Wings":{"BACTOM":{"Code":"BACTOM","Description":"Tender bites of lightly breaded, 100% whole breast white meat chicken, topped with garlic parmesan white sauce, a blend of cheese made with mozzarella and cheddar, crispy bacon and tomato.","Local":false,"Name":"Specialty Chicken – Crispy Bacon & Tomato","SortSeq":"01"},"HOTBUFF":{"Code":"HOTBUFF","Description":"Tender bites of lightly breaded, 100% whole breast white meat chicken, topped with classic hot buffalo sauce, ranch, a blend of cheese made with mozzarella and cheddar, and feta.","Local":false,"Name":"Specialty Chicken – Classic Hot Buffalo","SortSeq":"02"},"SPCYJP":{"Code":"SPCYJP","Description":"Tender bites of lightly breaded, 100% whole breast white meat chicken, topped with sweet and spicy mango-habanero sauce, a blend of cheese made with mozzarella and cheddar, jalapeno and pineapple.","Local":false,"Name":"Specialty Chicken – Spicy Jalapeno - Pineapple","SortSeq":"03"},"BBQBAC":{"Code":"BBQBAC","Description":"Tender bites of lightly breaded, 100% whole breast white meat chicken, topped with sweet and smoky BBQ sauce, a blend of cheese made with mozzarella and cheddar, and crispy bacon.","Local":false,"Name":"Specialty Chicken – Sweet BBQ Bacon","SortSeq":"04"},"HOTWINGS":{"Code":"HOTWINGS","Description":"Marinated and oven-baked and then smothered in Hot Sauce. Customize with your choice of dipping sauce: Sweet Mango Habanero, BBQ, Kicker Hot Sauce, Blue Cheese, or Ranch","Local":false,"Name":"Hot Wings","SortSeq":"06"},"SMANG":{"Code":"SMANG","Description":"Marinated and oven-baked and then smothered in Sweet Mango Habanero Sauce. Customize with your choice of dipping sauce: Sweet Mango Habanero, BBQ, Kicker Hot Sauce, Blue Cheese, or Ranch","Local":false,"Name":"Sweet Mango Habanero Wings","SortSeq":"08"},"BBQW":{"Code":"BBQW","Description":"Marinated and oven-baked and then smothered in BBQ Sauce. Customize with your choice of dipping sauce: Sweet Mango Habanero, BBQ, Kicker Hot Sauce, Blue Cheese, or Ranch","Local":false,"Name":"BBQ Wings","SortSeq":"09"},"PLNWINGS":{"Code":"PLNWINGS","Description":"Marinated and oven-baked and then sauced with your choice of Hot, Sweet Mango Habanero or BBQ sauce.","Local":false,"Name":"Plain Wings","SortSeq":"10"},"BCHICK":{"Code":"BCHICK","Description":"Lightly breaded with savory herbs, made with 100% whole white breast meat. Customize with your choice of dipping sauce: Sweet Mango Habanero, BBQ, Kicker Hot Sauce, Blue Cheese or Ranch.","Local":false,"Name":"Boneless Chicken","SortSeq":"20"}}},"Products":{"F_PARMT":{"AvailableToppings":"","AvailableSides":"SIDMAR,SIDGAR,SIDRAN,Bd","Code":"F_PARMT","DefaultToppings":"","DefaultSides":"SIDMAR=1","Description":"Handmade from fresh buttery-tasting dough and baked to a golden brown. Crusty on the outside and soft on the inside. Drizzled with garlic and Parmesan cheese seasoning, and sprinkled with more Parmesan. Served with a side of marinara sauce for dipping.","ImageCode":"F_PARMT","Local":false,"Name":"Parmesan Bread Twists","ProductType":"Bread","Tags":{},"Variants":["B8PCPT"]},"F_GARLICT":{"AvailableToppings":"","AvailableSides":"SIDMAR,SIDGAR,SIDRAN,Bd","Code":"F_GARLICT","DefaultToppings":"","DefaultSides":"SIDMAR=1","Description":"Handmade from fresh buttery-tasting dough and baked to a golden brown. Crusty on the outside and soft on the inside. Drizzled with buttery garlic and Parmesan cheese seasoning. Served with a side of marinara sauce for dipping.","ImageCode":"F_GARLICT","Local":false,"Name":"Garlic Bread Twists","ProductType":"Bread","Tags":{},"Variants":["B8PCGT"]},"F_SCBRD":{"AvailableToppings":"","AvailableSides":"SIDMAR,SIDGAR,SIDRAN,Bd","Code":"F_SCBRD","DefaultToppings":"","DefaultSides":"","Description":"Our oven-baked breadsticks are generously stuffed and covered with a blend of 100% real mozzarella and cheddar cheeses then seasoned with a touch of garlic. Add marinara or your favorite dipping cup for an additional charge.","ImageCode":"F_SCBRD","Local":false,"Name":"Stuffed Cheesy Bread","ProductType":"Bread","Tags":{"BazaarVoice":true},"Variants":["B8PCSCB"]},"F_SSBRD":{"AvailableToppings":"Si=0:0.5:1,Fe=0:0.5:1","AvailableSides":"SIDMAR,SIDGAR,SIDRAN,Bd","Code":"F_SSBRD","DefaultToppings":"Si=1,Fe=1","DefaultSides":"","Description":"Our oven-baked breadsticks are stuffed with cheese, fresh spinach and Feta cheese - covered in a blend of cheese made with 100% real mozzarella and cheddar. Seasoned with a touch of garlic and Parmesan. Add marinara or your favorite dipping cup for an additional charge.","ImageCode":"F_SSBRD","Local":false,"Name":"Stuffed Cheesy Bread with Spinach & Feta","ProductType":"Bread","Tags":{"BazaarVoice":true},"Variants":["B8PCSSF"]},"F_SBBRD":{"AvailableToppings":"K=0:0.5:1,J=0:0.5:1","AvailableSides":"SIDMAR,SIDGAR,SIDRAN,Bd","Code":"F_SBBRD","DefaultToppings":"K=1,J=1","DefaultSides":"","Description":"Our oven-baked breadsticks are stuffed with cheese, smoked bacon & jalapeno peppers - covered in a blend of cheeses; made with 100% real mozzarella and cheddar. Seasoned with a touch of garlic and Parmesan. Add marinara or your favorite dipping cup for an additional charge.","ImageCode":"F_SBBRD","Local":false,"Name":"Stuffed Cheesy Bread with Bacon & Jalapeno","ProductType":"Bread","Tags":{"BazaarVoice":true},"Variants":["B8PCSBJ"]},"F_PBITES":{"AvailableToppings":"","AvailableSides":"SIDMAR,SIDGAR,SIDRAN,Bd","Code":"F_PBITES","DefaultToppings":"","DefaultSides":"SIDMAR=1","Description":"Oven-baked bread bites handmade from fresh buttery-tasting dough and seasoned with garlic and Parmesan. Available in 16-piece or 32-piece orders. Add marinara or your favorite dipping cup for an additional charge.","ImageCode":"F_PBITES","Local":false,"Name":"Parmesan Bread Bites","ProductType":"Bread","Tags":{"BazaarVoice":true},"Variants":["B16PBIT","B32PBIT"]},"F_CINNAT":{"AvailableToppings":"","AvailableSides":"SIDICE","Code":"F_CINNAT","DefaultToppings":"","DefaultSides":"SIDICE=1","Description":"Handmade from fresh buttery-tasting dough and baked to a golden brown. Crusty on the outside and soft on the inside. Drizzled with a perfect blend of cinnamon and sugar, and served with a side of sweet icing for dipping or drizzling.","ImageCode":"F_CINNAT","Local":false,"Name":"Cinnamon Bread Twists","ProductType":"Dessert","Tags":{},"Variants":["B8PCCT"]},"F_MRBRWNE":{"AvailableToppings":"","AvailableSides":"","Code":"F_MRBRWNE","DefaultToppings":"","DefaultSides":"","Description":"Satisfy your sweet tooth! Taste the decadent blend of gooey milk chocolate chunk cookie and delicious fudge brownie. Oven-baked to perfection and cut into 9 pieces - this dessert is perfect to share with the whole group.","ImageCode":"F_MRBRWNE","Local":false,"Name":"Domino's Marbled Cookie Brownie™","ProductType":"Dessert","Tags":{},"Variants":["MARBRWNE"]},"F_LAVA":{"AvailableToppings":"","AvailableSides":"SIDICE","Code":"F_LAVA","DefaultToppings":"","DefaultSides":"","Description":"Indulge in two delectable oven-baked chocolate cakes with molten chocolate fudge on the inside. Perfectly topped with a dash of powdered sugar.","ImageCode":"F_LAVA","Local":false,"Name":"Chocolate Lava Crunch Cakes","ProductType":"Dessert","Tags":{"BazaarVoice":true},"Variants":["B2PCLAVA"]},"F_COKE":{"AvailableToppings":"","AvailableSides":"","Code":"F_COKE","DefaultToppings":"","DefaultSides":"","Description":"The authentic cola sensation that is a refreshing part of sharing life's enjoyable moments.","ImageCode":"F_COKE","Local":false,"Name":"Coke®","ProductType":"Drinks","Tags":{},"Variants":["20BCOKE","2LCOKE"]},"F_ORAN":{"AvailableToppings":"","AvailableSides":"","Code":"F_ORAN","DefaultToppings":"","DefaultSides":"","Description":"Exuberant tropical fun to release you from the everyday mundane.","ImageCode":"F_ORAN","Local":false,"Name":"Fanta® Orange","ProductType":"Drinks","Tags":{},"Variants":["20BORNG","2LMMORANGE"]},"F_SPRITE":{"AvailableToppings":"","AvailableSides":"","Code":"F_SPRITE","DefaultToppings":"","DefaultSides":"","Description":"Unique Lymon (lemon-lime) flavor, clear, clean and crisp with no caffeine.","ImageCode":"F_SPRITE","Local":false,"Name":"Sprite®","ProductType":"Drinks","Tags":{},"Variants":["20BSPRITE","2LSPRITE"]},"F_DIET":{"AvailableToppings":"","AvailableSides":"","Code":"F_DIET","DefaultToppings":"","DefaultSides":"","Description":"Beautifully balanced adult cola taste in a no calorie beverage.","ImageCode":"F_DIET","Local":false,"Name":"Diet Coke®","ProductType":"Drinks","Tags":{},"Variants":["2LDCOKE","20BDCOKE"]},"F_WATER":{"AvailableToppings":"","AvailableSides":"","Code":"F_WATER","DefaultToppings":"","DefaultSides":"","Description":"Fresh, crisp tasting water.","ImageCode":"F_WATER","Local":false,"Name":"Dasani® Bottle Water","ProductType":"Drinks","Tags":{},"Variants":["BOTTLWATER"]},"F_FITLEM":{"AvailableToppings":"","AvailableSides":"","Code":"F_FITLEM","DefaultToppings":"","DefaultSides":"","Description":"Naturally flavored with lemon to deliver bold refreshment.","ImageCode":"F_FITLEM","Local":true,"Name":"FUZE® Iced Tea Lemon","ProductType":"Drinks","Tags":{},"Variants":["D20BFITLEM"]},"F_GARDEN":{"AvailableToppings":"","AvailableSides":"CAESAR,ITAL,BALVIN,RANCHPK","Code":"F_GARDEN","DefaultToppings":"","DefaultSides":"RANCHPK=1","Description":"A crisp and colorful combination of grape tomatoes, red onion, carrots, red cabbage, cheddar cheese and brioche garlic croutons, all atop a blend of romaine and iceberg lettuce.","ImageCode":"F_GARDEN","Local":false,"Name":"Classic Garden","ProductType":"GSalad","Tags":{},"Variants":["PPSGARSA"]},"F_CCAESAR":{"AvailableToppings":"","AvailableSides":"CAESAR,ITAL,BALVIN,RANCHPK","Code":"F_CCAESAR","DefaultToppings":"","DefaultSides":"CAESAR=1","Description":"The makings of a classic: roasted white meat chicken, Parmesan cheese and brioche garlic croutons, all atop a blend of romaine and iceberg lettuce.","ImageCode":"F_CCAESAR","Local":false,"Name":"Chicken Caesar","ProductType":"GSalad","Tags":{},"Variants":["PPSCSRSA"]},"S_BUILD":{"AvailableToppings":"Xf=0:1,Xm=0:1,P,S,B,Pm,H,K,Du,C,E,Fe,Cs,Cp,F,G,J,M,N,O,R,Rr,Si,Td,Z","AvailableSides":"","Code":"S_BUILD","DefaultToppings":"Xf=1","DefaultSides":"","Description":"Choose a sauce and up to 3 ingredients from more than a dozen meat or vegetable toppings.","ImageCode":"S_BUILD","Local":false,"Name":"Build Your Own Pasta","ProductType":"Pasta","Tags":{"OptionQtys":["0","1"],"MaxOptionQty":"3","IsDisplayedOnMakeline":true,"SauceRequired":true},"Variants":["PINPASBD","PINBBLBD"]},"S_ALFR":{"AvailableToppings":"Xf=1,Du","AvailableSides":"","Code":"S_ALFR","DefaultToppings":"Du=1,Xf=1","DefaultSides":"","Description":"Try our savory Chicken Alfredo Pasta. Grilled chicken breast and creamy Alfredo sauce is mixed with penne pasta and baked to creamy perfection.","ImageCode":"S_ALFR","Local":false,"Name":"Chicken Alfredo","ProductType":"Pasta","Tags":{"OptionQtys":["0","1"],"MaxOptionQty":"3","IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["PINPASCA","PINBBLCA"]},"S_CARB":{"AvailableToppings":"Xf=1,K,Du,M,O","AvailableSides":"","Code":"S_CARB","DefaultToppings":"M=1,O=1,Du=1,K=1,Xf=1","DefaultSides":"","Description":"Taste the delectable blend of flavorful grilled chicken breast, smoked bacon, fresh onions, and fresh mushrooms mixed with penne pasta. Baked to perfection with rich Alfredo sauce.","ImageCode":"S_CARB","Local":false,"Name":"Chicken Carbonara","ProductType":"Pasta","Tags":{"OptionQtys":["0","1"],"MaxOptionQty":"4","IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["PINPASCC","PINBBLCC"]},"S_MARIN":{"AvailableToppings":"Xm=1,S,Cp","AvailableSides":"","Code":"S_MARIN","DefaultToppings":"S=1,Cp=1,Xm=1","DefaultSides":"","Description":"Penne pasta baked in a zesty tomato basil marinara sauce with Italian sausage, a blend of Italian seasonings and provolone cheese.","ImageCode":"S_MARIN","Local":false,"Name":"Italian Sausage Marinara","ProductType":"Pasta","Tags":{"OptionQtys":["0","1"],"MaxOptionQty":"3","IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["PINPASMM","PINBBLMM"]},"S_PRIM":{"AvailableToppings":"Xf=1,M,O,Si,Td","AvailableSides":"","Code":"S_PRIM","DefaultToppings":"M=1,O=1,Td=1,Si=1,Xf=1","DefaultSides":"","Description":"Fresh baby spinach, diced tomatoes, fresh mushrooms and fresh onions, mixed with penne pasta and baked with a creamy Alfredo sauce.","ImageCode":"S_PRIM","Local":false,"Name":"Pasta Primavera","ProductType":"Pasta","Tags":{"OptionQtys":["0","1"],"MaxOptionQty":"4","IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["PINPASPP","PINBBLPP"]},"S_DX":{"AvailableToppings":"X=0:0.5:1:1.5,Xm=0:0.5:1:1.5,Bq,Xw=0:0.5:1:1.5,C,H,B,Sa,P,S,Du,K,Pm,Ht,F,J,O,Z,Td,R,M,N,Cp,E,G,Si,Rr,Fe,Cs,Xf=0:0.5:1:1.5","AvailableSides":"","Code":"S_DX","DefaultToppings":"X=1,C=1,P=1,M=1,O=1,G=1,S=1","DefaultSides":"","Description":"Pepperoni, Italian sausage, fresh green peppers, fresh mushrooms, fresh onions and cheese made with 100% real mozzarella.","ImageCode":"S_DX","Local":false,"Name":"Deluxe","ProductType":"Pizza","Tags":{"OptionQtys":["0","0.5","1","1.5","2"],"MaxOptionQty":"10","PartCount":"2","NeedsCustomization":true,"CouponTier":["MultiplePizza","MultiplePizzaB","MultiplePizzaC","MultiplePizzaD"],"IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["10SCDELUX","10TDELUX","12SCDELUX","12TDELUX","PBKIREDX","14SCDELUX","14TDELUX","P16IBKDX","P10IGFDX","P12IPADX"]},"S_MX":{"AvailableToppings":"X=0:0.5:1:1.5,Xm=0:0.5:1:1.5,Bq,Xw=0:0.5:1:1.5,C,H,B,Sa,P,S,Du,K,Pm,Ht,F,J,O,Z,Td,R,M,N,Cp,E,G,Si,Rr,Fe,Cs,Xf=0:0.5:1:1.5","AvailableSides":"","Code":"S_MX","DefaultToppings":"X=1,C=1.5,P=1,H=1,S=1,B=1","DefaultSides":"","Description":"Pepperoni, ham, Italian sausage and beef, all sandwiched between two layers of cheese made with 100% real mozzarella.","ImageCode":"S_MX","Local":false,"Name":"MeatZZa","ProductType":"Pizza","Tags":{"OptionQtys":["0","0.5","1","1.5","2"],"MaxOptionQty":"10","PartCount":"2","NeedsCustomization":true,"CouponTier":["MultiplePizza","MultiplePizzaB","MultiplePizzaC","MultiplePizzaD"],"IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["10SCMEATZA","10TMEATZA","12SCMEATZA","12TMEATZA","PBKIREMX","14SCMEATZA","14TMEATZA","P16IBKMX","P10IGFMX","P12IPAMX"]},"S_PIZBP":{"AvailableToppings":"H,B,Sa,P,S,Du,K,Pm,Ht,F,J,O,Z,Td,R,M,N,Cp,E,G,Si,Rr,Fe,Cs,Ac","AvailableSides":"","Code":"S_PIZBP","DefaultToppings":"O=1,Du=1,E=1,Cp=1,Ac=1,Ht=1","DefaultSides":"","Description":"Grilled chicken breast, fresh onions, provolone, American cheese, cheddar, cheese made with 100% real mozzarella and drizzled with a hot sauce.","ImageCode":"S_PIZBP","Local":false,"Name":"Buffalo Chicken","ProductType":"Pizza","Tags":{"OptionQtys":["0","0.5","1","1.5","2"],"MaxOptionQty":"10","PartCount":"2","NeedsCustomization":true,"CouponTier":["MultiplePizza","MultiplePizzaB","MultiplePizzaC","MultiplePizzaD"],"IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["P10IREBP","P10ITHBP","P12IREBP","P12ITHBP","P14IBKBP","P14IREBP","P14ITHBP","P16IBKBP","P10IGFBP","P12IPABP"]},"S_PIZCK":{"AvailableToppings":"X=0:0.5:1:1.5,Xm=0:0.5:1:1.5,Bq,Xw=0:0.5:1:1.5,C,H,B,Sa,P,S,Du,K,Pm,Ht,F,J,O,Z,Td,R,M,N,Cp,E,G,Si,Rr,Fe,Cs,Xf=0:0.5:1:1.5","AvailableSides":"","Code":"S_PIZCK","DefaultToppings":"C=1,Bq=1,O=1,Du=1,E=1,Cp=1","DefaultSides":"","Description":"Grilled chicken breast, BBQ sauce, fresh onions, cheddar, provolone and cheese made with 100% real mozzarella.","ImageCode":"S_PIZCK","Local":false,"Name":"Memphis BBQ Chicken","ProductType":"Pizza","Tags":{"OptionQtys":["0","0.5","1","1.5","2"],"MaxOptionQty":"10","PartCount":"2","NeedsCustomization":true,"CouponTier":["MultiplePizza","MultiplePizzaB","MultiplePizzaC","MultiplePizzaD"],"IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["P10IRECK","P10ITHCK","P12IRECK","P12ITHCK","P14IBKCK","P14IRECK","P14ITHCK","P16IBKCK","P10IGFCK","P12IPACK"]},"S_PIZCR":{"AvailableToppings":"X=0:0.5:1:1.5,Xm=0:0.5:1:1.5,Bq,Xw=0:0.5:1:1.5,C,H,B,Sa,P,S,Du,K,Pm,Ht,F,J,O,Z,Td,R,M,N,Cp,E,G,Si,Rr,Fe,Cs,Xf=0:0.5:1:1.5","AvailableSides":"","Code":"S_PIZCR","DefaultToppings":"C=1,Xw=1,Du=1,K=1,Td=1,Cp=1","DefaultSides":"","Description":"Grilled chicken breast, garlic Parmesan white sauce, smoked bacon, tomatoes, provolone and cheese made with 100% real mozzarella.","ImageCode":"S_PIZCR","Local":false,"Name":"Cali Chicken Bacon Ranch","ProductType":"Pizza","Tags":{"OptionQtys":["0","0.5","1","1.5","2"],"MaxOptionQty":"10","PartCount":"2","NeedsCustomization":true,"CouponTier":["MultiplePizza","MultiplePizzaB","MultiplePizzaC","MultiplePizzaD"],"IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["P10IRECR","P10ITHCR","P12IRECR","P12ITHCR","P14IBKCR","P14IRECR","P14ITHCR","P16IBKCR","P10IGFCR","P12IPACR"]},"S_PIZCZ":{"AvailableToppings":"X=0:0.5:1:1.5,Xm=0:0.5:1:1.5,Bq,Xw=0:0.5:1:1.5,C,H,B,Sa,P,S,Du,K,Pm,Ht,F,J,O,Z,Td,R,M,N,Cp,E,G,Si,Rr,Fe,Cs,Xf=0:0.5:1:1.5","AvailableSides":"","Code":"S_PIZCZ","DefaultToppings":"X=1,C=1,E=1,Fe=1,Cs=1,Cp=1","DefaultSides":"","Description":"Feta, provolone, cheddar, Parmesan-Asiago, cheese made with 100% real mozzarella and sprinkled with oregano.","ImageCode":"S_PIZCZ","Local":false,"Name":"Wisconsin 6 Cheese","ProductType":"Pizza","Tags":{"OptionQtys":["0","0.5","1","1.5","2"],"MaxOptionQty":"10","PartCount":"2","NeedsCustomization":true,"CouponTier":["MultiplePizza","MultiplePizzaB","MultiplePizzaC","MultiplePizzaD"],"IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["P10IRECZ","P10ITHCZ","P12IRECZ","P12ITHCZ","P14IBKCZ","P14IRECZ","P14ITHCZ","P16IBKCZ","P10IGFCZ","P12IPACZ"]},"S_PIZPH":{"AvailableToppings":"H,B,Sa,P,S,Du,K,Pm,F,J,O,Z,Td,R,M,N,Cp,E,G,Si,Rr,Fe,Cs,Ac","AvailableSides":"","Code":"S_PIZPH","DefaultToppings":"M=1,O=1,G=1,Pm=1,Cp=1,Ac=1","DefaultSides":"","Description":"Tender slices of steak, fresh onions, fresh green peppers, fresh mushrooms, provolone and American cheese.","ImageCode":"S_PIZPH","Local":false,"Name":"Philly Cheese Steak","ProductType":"Pizza","Tags":{"OptionQtys":["0","0.5","1","1.5","2"],"MaxOptionQty":"10","PartCount":"2","NeedsCustomization":true,"CouponTier":["MultiplePizza","MultiplePizzaB","MultiplePizzaC","MultiplePizzaD"],"IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["P10IREPH","P10ITHPH","P12IREPH","P12ITHPH","P14IBKPH","P14IREPH","P14ITHPH","P16IBKPH","P10IGFPH","P12IPAPH"]},"S_PIZPV":{"AvailableToppings":"X=0:0.5:1:1.5,Xm=0:0.5:1:1.5,Bq,Xw=0:0.5:1:1.5,C,H,B,Sa,P,S,Du,K,Pm,Ht,F,J,O,Z,Td,R,M,N,Cp,E,G,Si,Rr,Fe,Cs,Xf=0:0.5:1:1.5","AvailableSides":"","Code":"S_PIZPV","DefaultToppings":"X=1,C=1,M=1,O=1,R=1,Td=1,Rr=1,Si=1,Fe=1,Cp=1","DefaultSides":"","Description":"Roasted red peppers, fresh baby spinach, fresh onions, fresh mushrooms, tomatoes, black olives, feta, provolone, cheese made with 100% real mozzarella and sprinkled with a garlic herb seasoning.","ImageCode":"S_PIZPV","Local":false,"Name":"Pacific Veggie","ProductType":"Pizza","Tags":{"OptionQtys":["0","0.5","1","1.5","2"],"MaxOptionQty":"10","PartCount":"2","NeedsCustomization":true,"CouponTier":["MultiplePizza","MultiplePizzaB","MultiplePizzaC","MultiplePizzaD"],"IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["P10IREPV","P10ITHPV","P12IREPV","P12ITHPV","P14IBKPV","P14IREPV","P14ITHPV","P16IBKPV","P10IGFPV","P12IPAPV"]},"S_PIZPX":{"AvailableToppings":"X=0:0.5:1:1.5,Xm=0:0.5:1:1.5,Bq,Xw=0:0.5:1:1.5,C,H,B,Sa,P,S,Du,K,Pm,Ht,F,J,O,Z,Td,R,M,N,Cp,E,G,Si,Rr,Fe,Cs,Xf=0:0.5:1:1.5","AvailableSides":"","Code":"S_PIZPX","DefaultToppings":"X=1,C=1,P=1.5,Cs=1,Cp=1","DefaultSides":"","Description":"Two layers of pepperoni sandwiched between provolone, Parmesan-Asiago and cheese made with 100% real mozzarella then sprinkled with oregano.","ImageCode":"S_PIZPX","Local":false,"Name":"Ultimate Pepperoni","ProductType":"Pizza","Tags":{"OptionQtys":["0","0.5","1","1.5","2"],"MaxOptionQty":"10","PartCount":"2","NeedsCustomization":true,"CouponTier":["MultiplePizza","MultiplePizzaB","MultiplePizzaC","MultiplePizzaD"],"IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["10SCPFEAST","10TPFEAST","12SCPFEAST","12TPFEAST","PBKIREPX","14SCPFEAST","14TPFEAST","P16IBKPX","P10IGFPX","P12IPAPX"]},"S_PIZUH":{"AvailableToppings":"X=0:0.5:1:1.5,Xm=0:0.5:1:1.5,Bq,Xw=0:0.5:1:1.5,C,H,B,Sa,P,S,Du,K,Pm,Ht,F,J,O,Z,Td,R,M,N,Cp,E,G,Si,Rr,Fe,Cs,Xf=0:0.5:1:1.5","AvailableSides":"","Code":"S_PIZUH","DefaultToppings":"X=1,C=1,H=1,N=1,K=1,Rr=1,Cp=1","DefaultSides":"","Description":"Sliced ham, smoked bacon, pineapple, roasted red peppers, provolone and cheese made with 100% real mozzarella.","ImageCode":"S_PIZUH","Local":false,"Name":"Honolulu Hawaiian","ProductType":"Pizza","Tags":{"OptionQtys":["0","0.5","1","1.5","2"],"MaxOptionQty":"10","PartCount":"2","NeedsCustomization":true,"CouponTier":["MultiplePizza","MultiplePizzaB","MultiplePizzaC","MultiplePizzaD"],"IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["P10IREUH","P10ITHUH","P12IREUH","P12ITHUH","P14IBKUH","P14IREUH","P14ITHUH","P16IBKUH","P10IGFUH","P12IPAUH"]},"S_PIZZA":{"AvailableToppings":"X=0:0.5:1:1.5,Xm=0:0.5:1:1.5,Bq,Xw=0:0.5:1:1.5,C,B,Sa,P,S,Du,K,Pm,Ht,F,J,O,Z,Td,R,M,N,Cp,E,G,Si,Rr,Fe,Cs,Xf=0:0.5:1:1.5","AvailableSides":"","Code":"S_PIZZA","DefaultToppings":"X=1,C=1","DefaultSides":"","Description":"A custom pizza made to order. Choose from any of our delicious crust styles, including Handmade Pan.","ImageCode":"S_PIZZA","Local":false,"Name":"Pizza","ProductType":"Pizza","Tags":{"OptionQtys":["0","0.5","1","1.5","2"],"MaxOptionQty":"10","PartCount":"2","NeedsCustomization":true,"CouponTier":["MultiplePizza","MultiplePizzaB","MultiplePizzaC","MultiplePizzaD"],"IsDisplayedOnMakeline":true,"sodiumWarningEnabled":true},"Variants":["10SCREEN","10THIN","12SCREEN","12THIN","PBKIREZA","14SCREEN","14THIN","P16IBKZA","P10IGFZA","P12IPAZA"]},"S_ZZ":{"AvailableToppings":"X=0:0.5:1:1.5,Xm=0:0.5:1:1.5,Bq,Xw=0:0.5:1:1.5,C,H,B,Sa,P,S,Du,K,Pm,Ht,F,J,O,Z,Td,R,M,N,Cp,E,G,Si,Rr,Fe,Cs,Xf=0:0.5:1:1.5","AvailableSides":"","Code":"S_ZZ","DefaultToppings":"X=1,C=1.5,P=1,H=1,M=1,O=1,G=1,R=1,S=1,B=1","DefaultSides":"","Description":"Pepperoni, ham, Italian sausage, beef, fresh onions, fresh green peppers, fresh mushrooms and black olives, all sandwiched between two layers of cheese made with 100% real mozzarella.","ImageCode":"S_ZZ","Local":false,"Name":"ExtravaganZZa","ProductType":"Pizza","Tags":{"OptionQtys":["0","0.5","1","1.5","2"],"MaxOptionQty":"10","PartCount":"2","NeedsCustomization":true,"CouponTier":["MultiplePizza","MultiplePizzaB","MultiplePizzaC","MultiplePizzaD"],"IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["10SCEXTRAV","10TEXTRAV","12SCEXTRAV","12TEXTRAV","PBKIREZZ","14SCEXTRAV","14TEXTRAV","P16IBKZZ","P10IGFZZ","P12IPAZZ"]},"S_PISPF":{"AvailableToppings":"X=0:0.5:1:1.5,Xm=0:0.5:1:1.5,Bq,Xw=0:0.5:1:1.5,C,H,B,Sa,P,S,Du,K,Pm,Ht,F,J,O,Z,Td,R,M,N,Cp,E,G,Si,Rr,Fe,Cs,Xf=0:0.5:1:1.5","AvailableSides":"","Code":"S_PISPF","DefaultToppings":"C=1,O=1,Si=1,Fe=1,Cs=1,Cp=1,Xf=1","DefaultSides":"","Description":"Creamy Alfredo sauce, fresh spinach, fresh onions, feta, Parmesan-Asiago, provolone and cheese made with 100% real mozzarella.","ImageCode":"S_PISPF","Local":false,"Name":"Spinach & Feta","ProductType":"Pizza","Tags":{"OptionQtys":["0","0.5","1","1.5","2"],"MaxOptionQty":"10","PartCount":"2","NeedsCustomization":true,"CouponTier":["MultiplePizza","MultiplePizzaB","MultiplePizzaC","MultiplePizzaD"],"IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["P10IRESPF","P10ITHSPF","P10IGFSPF","P12IRESPF","P12ITHSPF","P12IPASPF","P14IBKSPF","P14IRESPF","P14ITHSPF","P16IBKSPF"]},"S_CHIKK":{"AvailableToppings":"Rd=0:0.5:1,C=0:0.5:1,K=0:0.5:1,Du,Pv","AvailableSides":"","Code":"S_CHIKK","DefaultToppings":"C=1,Du=1,K=1,Pv=1,Rd=1","DefaultSides":"","Description":"Enjoy our flavorful grilled chicken breast topped with smoked bacon, creamy ranch and provolone cheese on artisan bread baked to golden brown perfection.","ImageCode":"S_CHIKK","Local":false,"Name":"Chicken Bacon Ranch","ProductType":"Sandwich","Tags":{"OptionQtys":["0","0.5","1","1.5"],"MaxOptionQty":"9","MaxSauceQty":"2","IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["PSANSACB"]},"S_CHIKP":{"AvailableToppings":"X=0:0.5:1,C=0:0.5:1,Du,Cs=0:0.5:1,Pv","AvailableSides":"","Code":"S_CHIKP","DefaultToppings":"X=1,C=1,Du=1,Cs=1,Pv=1","DefaultSides":"","Description":"Grilled chicken breast, tomato basil marinara, Parmesan-Asiago and provolone cheese. On artisan bread and baked to a golden brown.","ImageCode":"S_CHIKP","Local":false,"Name":"Chicken Parm","ProductType":"Sandwich","Tags":{"OptionQtys":["0","0.5","1","1.5"],"MaxOptionQty":"9","MaxSauceQty":"2","IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["PSANSACP"]},"S_ITAL":{"AvailableToppings":"C=0:0.5:1,P,H=0:0.5:1,Sa=0:0.5:1,Pv,Z=0:0.5:1,G=0:0.5:1,O=0:0.5:1","AvailableSides":"","Code":"S_ITAL","DefaultToppings":"C=1,P=1,H=1,O=1,G=1,Z=1,Sa=1,Pv=1","DefaultSides":"","Description":"Pepperoni, salami, and ham topped with banana peppers, fresh green peppers, fresh onions, and provolone cheese. On artisan bread and baked to a golden brown.","ImageCode":"S_ITAL","Local":false,"Name":"Italian","ProductType":"Sandwich","Tags":{"OptionQtys":["0","0.5","1","1.5"],"MaxOptionQty":"9","MaxSauceQty":"2","IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["PSANSAIT"]},"S_PHIL":{"AvailableToppings":"Pm,Ac=0:0.5:1,Pv,G=0:0.5:1,M=0:0.5:1,O=0:0.5:1","AvailableSides":"","Code":"S_PHIL","DefaultToppings":"M=1,O=1,G=1,Pm=1,Ac=1,Pv=1","DefaultSides":"","Description":"Experience deliciously tender slices of steak, American and provolone cheeses, fresh onions, fresh green peppers and fresh mushrooms placed on artisan bread and baked to golden brown perfection.","ImageCode":"S_PHIL","Local":false,"Name":"Philly Cheese Steak","ProductType":"Sandwich","Tags":{"OptionQtys":["0","0.5","1","1.5"],"MaxOptionQty":"9","MaxSauceQty":"2","IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["PSANSAPH"]},"S_BUFC":{"AvailableToppings":"Bd=0:0.5:1,Ht=0:0.5:1,C=0:0.5:1,Du,E=0:0.5:1,Pv,O=0:0.5:1","AvailableSides":"","Code":"S_BUFC","DefaultToppings":"C=1,O=1,Du=1,E=1,Ht=1,Pv=1,Bd=1","DefaultSides":"","Description":"Grilled chicken breast, creamy blue cheese sauce, fresh onions, hot sauce, cheddar and provolone cheeses. On artisan bread and baked to a golden brown.","ImageCode":"S_BUFC","Local":false,"Name":"Buffalo Chicken","ProductType":"Sandwich","Tags":{"OptionQtys":["0","0.5","1","1.5"],"MaxOptionQty":"9","MaxSauceQty":"2","IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["PSANSABC"]},"S_CHHB":{"AvailableToppings":"Mh=0:0.5:1,C=0:0.5:1,Du,E=0:0.5:1,Pv,J=0:0.5:1,N=0:0.5:1","AvailableSides":"","Code":"S_CHHB","DefaultToppings":"C=1,Du=1,N=1,E=1,J=1,Pv=1,Mh=1","DefaultSides":"","Description":"Grilled chicken breast, pineapple, jalapeños, sweet mango habanero sauce, provolone and cheddar cheeses. On artisan bread and baked to a golden brown.","ImageCode":"S_CHHB","Local":false,"Name":"Chicken Habanero","ProductType":"Sandwich","Tags":{"OptionQtys":["0","0.5","1","1.5"],"MaxOptionQty":"9","MaxSauceQty":"2","IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["PSANSACH"]},"S_MEDV":{"AvailableToppings":"Ac=0:0.5:1,Fe=0:0.5:1,Pv,Z=0:0.5:1,O=0:0.5:1,Rr=0:0.5:1,Si=0:0.5:1,Td=0:0.5:1","AvailableSides":"","Code":"S_MEDV","DefaultToppings":"O=1,Td=1,Rr=1,Si=1,Fe=1,Ac=1,Z=1,Pv=1","DefaultSides":"","Description":"Roasted red peppers, banana peppers, diced tomatoes, fresh baby spinach, fresh onions, feta, provolone and American cheese. On artisan bread and baked to a golden brown.","ImageCode":"S_MEDV","Local":false,"Name":"Mediterranean Veggie","ProductType":"Sandwich","Tags":{"OptionQtys":["0","0.5","1","1.5"],"MaxOptionQty":"9","MaxSauceQty":"2","IsDisplayedOnMakeline":true,"BazaarVoice":true},"Variants":["PSANSAMV"]},"F_SIDJAL":{"AvailableToppings":"","AvailableSides":"","Code":"F_SIDJAL","DefaultToppings":"","DefaultSides":"","Description":"Sliced jalapeno peppers","ImageCode":"F_SIDJAL","Local":true,"Name":"Side Jalapenos","ProductType":"Sides","Tags":{},"Variants":["SIDEJAL"]},"F_SIDPAR":{"AvailableToppings":"","AvailableSides":"","Code":"F_SIDPAR","DefaultToppings":"","DefaultSides":"","Description":"Grated Parmesan cheese packets","ImageCode":"F_SIDPAR","Local":true,"Name":"Parmesan Cheese Packets","ProductType":"Sides","Tags":{},"Variants":["PARMCHEESE"]},"F_SIDRED":{"AvailableToppings":"","AvailableSides":"","Code":"F_SIDRED","DefaultToppings":"","DefaultSides":"","Description":"Crushed red pepper flake packets","ImageCode":"F_SIDRED","Local":true,"Name":"Red Pepper Packets","ProductType":"Sides","Tags":{},"Variants":["REDPEPPER"]},"F_CAESAR":{"AvailableToppings":"","AvailableSides":"","Code":"F_CAESAR","DefaultToppings":"","DefaultSides":"","Description":"A savory dressing with a combination of garlic, anchovy and subtle notes of cheese.","ImageCode":"F_CAESAR","Local":false,"Name":"Caesar Dressing","ProductType":"Sides","Tags":{},"Variants":["AGCAESAR"]},"F_ITAL":{"AvailableToppings":"","AvailableSides":"","Code":"F_ITAL","DefaultToppings":"","DefaultSides":"","Description":"A classic dressing flavored with red bell pepper, a touch of garlic and spices.","ImageCode":"F_ITAL","Local":true,"Name":"Italian Dressing","ProductType":"Sides","Tags":{},"Variants":["AGITAL"]},"F_RANCHPK":{"AvailableToppings":"","AvailableSides":"","Code":"F_RANCHPK","DefaultToppings":"","DefaultSides":"","Description":"A creamy, flavorful dressing with a blend of buttermilk, garlic, onion and spices.","ImageCode":"F_RANCHPK","Local":false,"Name":"Ranch Dressing","ProductType":"Sides","Tags":{},"Variants":["AGRANCH"]},"F_HOTCUP":{"AvailableToppings":"","AvailableSides":"","Code":"F_HOTCUP","DefaultToppings":"","DefaultSides":"","Description":"Domino's own spicy Buffalo sauce","ImageCode":"F_HOTCUP","Local":false,"Name":"Kicker Hot Sauce","ProductType":"Sides","Tags":{},"Variants":["HOTSAUCE"]},"F_SMHAB":{"AvailableToppings":"","AvailableSides":"","Code":"F_SMHAB","DefaultToppings":"","DefaultSides":"","Description":"A perfect blend of sweet and spicy in one sauce","ImageCode":"F_SMHAB","Local":false,"Name":"Sweet Mango Habanero Sauce","ProductType":"Sides","Tags":{},"Variants":["CEAHABC"]},"F_BBQC":{"AvailableToppings":"","AvailableSides":"","Code":"F_BBQC","DefaultToppings":"","DefaultSides":"","Description":"A smoky BBQ sauce with bold flavor","ImageCode":"F_BBQC","Local":false,"Name":"BBQ Sauce","ProductType":"Sides","Tags":{},"Variants":["CEABBQC"]},"F_SIDRAN":{"AvailableToppings":"","AvailableSides":"","Code":"F_SIDRAN","DefaultToppings":"","DefaultSides":"","Description":"A creamy buttermilk ranch dressing with hints of garlic and onion","ImageCode":"F_SIDRAN","Local":false,"Name":"Ranch","ProductType":"Sides","Tags":{},"Variants":["RANCH"]},"F_Bd":{"AvailableToppings":"","AvailableSides":"","Code":"F_Bd","DefaultToppings":"","DefaultSides":"","Description":"A creamy dressing with bits of aged blue cheese","ImageCode":"F_Bd","Local":false,"Name":"Blue Cheese","ProductType":"Sides","Tags":{},"Variants":["BLUECHS"]},"F_SIDGAR":{"AvailableToppings":"","AvailableSides":"","Code":"F_SIDGAR","DefaultToppings":"","DefaultSides":"","Description":"A buttery garlic sauce","ImageCode":"F_SIDGAR","Local":false,"Name":"Garlic Dipping Sauce","ProductType":"Sides","Tags":{},"Variants":["GARBUTTER"]},"F_SIDICE":{"AvailableToppings":"","AvailableSides":"","Code":"F_SIDICE","DefaultToppings":"","DefaultSides":"","Description":"A thick sweet icing with a hint of vanilla","ImageCode":"F_SIDICE","Local":false,"Name":"Icing Dipping Sauce","ProductType":"Sides","Tags":{},"Variants":["ICING"]},"F_SIDMAR":{"AvailableToppings":"","AvailableSides":"","Code":"F_SIDMAR","DefaultToppings":"","DefaultSides":"","Description":"A sweet tomato sauce blended with garlic, basil and oregano","ImageCode":"F_SIDMAR","Local":false,"Name":"Marinara Dipping Sauce","ProductType":"Sides","Tags":{},"Variants":["MARINARA"]},"F_STJUDE":{"AvailableToppings":"","AvailableSides":"","Code":"F_STJUDE","DefaultToppings":"","DefaultSides":"","Description":"","ImageCode":"F_STJUDE","Local":false,"Name":"St. Jude Donation","ProductType":"Sides","Tags":{},"Variants":["STJUDE","STJUDE2","STJUDE5","STJUDE10","STJUDERU"]},"F_BALVIN":{"AvailableToppings":"","AvailableSides":"","Code":"F_BALVIN","DefaultToppings":"","DefaultSides":"","Description":"A light dressing with a blend of balsamic vinegar, oil and garlic.","ImageCode":"F_BALVIN","Local":false,"Name":"Balsamic","ProductType":"Sides","Tags":{},"Variants":["CEABVI"]},"F__SCHOOL":{"AvailableToppings":"","AvailableSides":"","Code":"F__SCHOOL","DefaultToppings":"","DefaultSides":"","Description":"Click here to add the local donation to your order","ImageCode":"F__SCHOOL","Local":true,"Name":"Local Donation","ProductType":"Sides","Tags":{},"Variants":["_SCHOOLL"]},"S_BONELESS":{"AvailableToppings":"","AvailableSides":"HOTCUP,SMHAB,BBQC,SIDRAN,Bd","Code":"S_BONELESS","DefaultToppings":"","DefaultSides":"HOTCUP=1","Description":"Lightly breaded with savory herbs, made with 100% whole white breast meat. Customize with your choice of dipping sauce: Sweet Mango Habanero, BBQ, Kicker Hot Sauce, Blue Cheese or Ranch.","ImageCode":"S_BONELESS","Local":false,"Name":"Boneless Chicken","ProductType":"Wings","Tags":{"OptionQtys":["0","0.5","1","1.5","2","3","4","5"],"MaxOptionQty":"99","IsDisplayedOnMakeline":true,"Boneless":true,"EffectiveOn":"2011-02-21","BvCode":"Boneless","BazaarVoice":true},"Variants":["W08PBNLW","W14PBNLW","W40PBNLW"]},"S_HOTWINGS":{"AvailableToppings":"","AvailableSides":"HOTCUP,SMHAB,BBQC,SIDRAN,Bd","Code":"S_HOTWINGS","DefaultToppings":"","DefaultSides":"Bd=1","Description":"Marinated and oven-baked and then smothered in Hot Sauce. Customize with your choice of dipping sauce: Sweet Mango Habanero, BBQ, Kicker Hot Sauce, Blue Cheese, or Ranch","ImageCode":"S_HOTWINGS","Local":false,"Name":"Hot Wings","ProductType":"Wings","Tags":{"OptionQtys":["0","0.5","1","1.5","2","3","4","5"],"MaxOptionQty":"99","IsDisplayedOnMakeline":true,"BundleBuilderProducts":true,"Wings":true,"EffectiveOn":"2011-02-21","sodiumWarningEnabled":true,"BvCode":"BoneIn","BazaarVoice":true},"Variants":["W08PHOTW","W14PHOTW","W40PHOTW"]},"S_BBQW":{"AvailableToppings":"","AvailableSides":"HOTCUP,SMHAB,BBQC,SIDRAN,Bd","Code":"S_BBQW","DefaultToppings":"","DefaultSides":"Bd=1","Description":"Marinated and oven-baked and then smothered in BBQ Sauce. Customize with your choice of dipping sauce: Sweet Mango Habanero, BBQ, Kicker Hot Sauce, Blue Cheese, or Ranch","ImageCode":"S_BBQW","Local":false,"Name":"BBQ Wings","ProductType":"Wings","Tags":{"OptionQtys":["0","0.5","1","1.5","2","3","4","5"],"MaxOptionQty":"99","IsDisplayedOnMakeline":true,"BundleBuilderProducts":true,"Wings":true,"EffectiveOn":"2011-02-21","BvCode":"BoneIn","BazaarVoice":true},"Variants":["W08PBBQW","W14PBBQW","W40PBBQW"]},"S_PLNWINGS":{"AvailableToppings":"","AvailableSides":"HOTCUP,SMHAB,BBQC,SIDRAN,Bd","Code":"S_PLNWINGS","DefaultToppings":"","DefaultSides":"Bd=1","Description":"Oven-baked to perfection. Customize with your choice of dipping sauce: Sweet Mango Habanero, BBQ, Kicker Hot Sauce, Blue Cheese, or Ranch","ImageCode":"S_PLNWINGS","Local":false,"Name":"Plain Wings","ProductType":"Wings","Tags":{"OptionQtys":["0","0.5","1","1.5","2","3","4","5"],"MaxOptionQty":"99","IsDisplayedOnMakeline":true,"BundleBuilderProducts":true,"Wings":true,"EffectiveOn":"2011-02-21","BvCode":"BoneIn","BazaarVoice":true},"Variants":["W08PPLNW","W14PPLNW","W40PPLNW"]},"S_SMANG":{"AvailableToppings":"","AvailableSides":"HOTCUP,SMHAB,BBQC,SIDRAN,Bd","Code":"S_SMANG","DefaultToppings":"","DefaultSides":"Bd=1","Description":"Marinated and oven-baked and then smothered in Sweet Mango Habanero Sauce. Customize with your choice of dipping sauce: Sweet Mango Habanero, BBQ, Kicker Hot Sauce, Blue Cheese, or Ranch","ImageCode":"S_SMANG","Local":false,"Name":"Sweet Mango Habanero Wings","ProductType":"Wings","Tags":{"OptionQtys":["0","0.5","1","1.5","2","3","4","5"],"MaxOptionQty":"99","IsDisplayedOnMakeline":true,"BundleBuilderProducts":true,"Wings":true,"EffectiveOn":"2011-02-21","BvCode":"BoneIn","BazaarVoice":true},"Variants":["W08PMANW","W14PMANW","W40PMANW"]},"S_SCCBT":{"AvailableToppings":"K=0:1,Td=0:1","AvailableSides":"HOTCUP,SMHAB,BBQC,SIDRAN,Bd","Code":"S_SCCBT","DefaultToppings":"K=1,Td=1","DefaultSides":"","Description":"Tender bites of lightly breaded, 100% whole breast white meat chicken, topped with garlic parmesan white sauce, a blend of cheese made with mozzarella and cheddar, crispy bacon and tomato.","ImageCode":"S_SCCBT","Local":false,"Name":"Specialty Chicken – Crispy Bacon & Tomato","ProductType":"Wings","Tags":{"OptionQtys":["0","0.5","1","1.5","2","3","4","5"],"MaxOptionQty":"99","IsDisplayedOnMakeline":true,"SpecialtyChicken":true,"Promotion":"SpChkProductNotInCart","PromotionType":"ProductNotInCart"},"Variants":["CKRGCBT"]},"S_SCCHB":{"AvailableToppings":"","AvailableSides":"HOTCUP,SMHAB,BBQC,SIDRAN,Bd","Code":"S_SCCHB","DefaultToppings":"","DefaultSides":"","Description":"Tender bites of lightly breaded, 100% whole breast white meat chicken, topped with classic hot buffalo sauce, ranch, a blend of cheese made with mozzarella and cheddar, and feta.","ImageCode":"S_SCCHB","Local":false,"Name":"Specialty Chicken – Classic Hot Buffalo","ProductType":"Wings","Tags":{"OptionQtys":["0","0.5","1","1.5","2","3","4","5"],"MaxOptionQty":"99","IsDisplayedOnMakeline":true,"SpecialtyChicken":true,"Promotion":"SpChkProductNotInCart","PromotionType":"ProductNotInCart"},"Variants":["CKRGHTB"]},"S_SCSJP":{"AvailableToppings":"J=0:1,N=0:1","AvailableSides":"HOTCUP,SMHAB,BBQC,SIDRAN,Bd","Code":"S_SCSJP","DefaultToppings":"J=1,N=1","DefaultSides":"","Description":"Tender bites of lightly breaded, 100% whole breast white meat chicken, topped with sweet and spicy mango-habanero sauce, a blend of cheese made with mozzarella and cheddar, jalapeno and pineapple.","ImageCode":"S_SCSJP","Local":false,"Name":"Specialty Chicken – Spicy Jalapeno - Pineapple","ProductType":"Wings","Tags":{"OptionQtys":["0","0.5","1","1.5","2","3","4","5"],"MaxOptionQty":"99","IsDisplayedOnMakeline":true,"SpecialtyChicken":true,"Promotion":"SpChkProductNotInCart","PromotionType":"ProductNotInCart"},"Variants":["CKRGSJP"]},"S_SCSBBQ":{"AvailableToppings":"K=0:1","AvailableSides":"HOTCUP,SMHAB,BBQC,SIDRAN,Bd","Code":"S_SCSBBQ","DefaultToppings":"K=1","DefaultSides":"","Description":"Tender bites of lightly breaded, 100% whole breast white meat chicken, topped with sweet and smoky BBQ sauce, a blend of cheese made with mozzarella and cheddar, and crispy bacon.","ImageCode":"S_SCSBBQ","Local":false,"Name":"Specialty Chicken – Sweet BBQ Bacon","ProductType":"Wings","Tags":{"OptionQtys":["0","0.5","1","1.5","2","3","4","5"],"MaxOptionQty":"99","IsDisplayedOnMakeline":true,"SpecialtyChicken":true,"Promotion":"SpChkProductNotInCart","PromotionType":"ProductNotInCart"},"Variants":["CKRGSBQ"]}},"Sides":{"Bread":{"SIDMAR":{"Availability":[],"Code":"SIDMAR","Description":"A sweet tomato sauce blended with garlic, basil and oregano","Local":false,"Name":"Marinara","Tags":{"Side":true}},"SIDGAR":{"Availability":[],"Code":"SIDGAR","Description":"A buttery garlic sauce","Local":true,"Name":"Garlic Dipping Sauce","Tags":{"Side":true}},"SIDRAN":{"Availability":[],"Code":"SIDRAN","Description":"A creamy buttermilk ranch dressing with hints of garlic and onion","Local":true,"Name":"Ranch","Tags":{"Side":true}},"Bd":{"Availability":[],"Code":"Bd","Description":"A creamy dressing with bits of aged blue cheese","Local":true,"Name":"Blue Cheese","Tags":{"Side":true}}},"Dessert":{"SIDICE":{"Availability":[],"Code":"SIDICE","Description":"","Local":false,"Name":"Icing","Tags":{"Side":true}}},"GSalad":{"CAESAR":{"Availability":[],"Code":"CAESAR","Description":"A subtle combination of Parmesan cheese, olive oil, lemon, garlic, onion and black pepper.","Local":false,"Name":"Caesar","Tags":{"Side":true}},"ITAL":{"Availability":[],"Code":"ITAL","Description":"A classic dressing flavored with spices, red bell pepper and a touch of garlic.","Local":true,"Name":"Italian","Tags":{"Side":true}},"BALVIN":{"Availability":[],"Code":"BALVIN","Description":"A light dressing with a blend of balsamic vinegar, oil and garlic.","Local":false,"Name":"Balsamic","Tags":{"Side":true}},"RANCHPK":{"Availability":[],"Code":"RANCHPK","Description":"A flavorful creamy dressing with touches of buttermilk and garlic.","Local":false,"Name":"Ranch","Tags":{"Side":true}}},"Wings":{"HOTCUP":{"Availability":[],"Code":"HOTCUP","Description":"Domino's own spicy Buffalo sauce","Local":false,"Name":"Kicker Hot Sauce","Tags":{"Side":true}},"SMHAB":{"Availability":[],"Code":"SMHAB","Description":"A perfect blend of sweet and spicy in one sauce","Local":false,"Name":"Sweet Mango Habanero Sauce","Tags":{"Side":true,"EffectiveOn":"2010-01-01"}},"BBQC":{"Availability":[],"Code":"BBQC","Description":"A smoky BBQ sauce with bold flavor","Local":false,"Name":"BBQ Sauce","Tags":{"Side":true,"EffectiveOn":"2010-01-01"}},"SIDRAN":{"Availability":[],"Code":"SIDRAN","Description":"A creamy buttermilk ranch dressing with hints of garlic and onion","Local":false,"Name":"Ranch","Tags":{"Side":true}},"Bd":{"Availability":[],"Code":"Bd","Description":"A creamy dressing with bits of aged blue cheese","Local":false,"Name":"Blue Cheese","Tags":{"Side":true}}}},"Sizes":{"Bread":{"BRD8":{"Code":"BRD8","Description":"","Local":false,"Name":"8-Piece","SortSeq":"02"},"BRD16":{"Code":"BRD16","Description":"","Local":false,"Name":"16-Piece","SortSeq":"06"},"BRD32":{"Code":"BRD32","Description":"","Local":false,"Name":"32-Piece","SortSeq":"07"}},"CHARGES":{"CHGONE":{"Code":"CHGONE","Description":"","Local":false,"Name":"Each","SortSeq":"01"}},"Dessert":{"DRT2":{"Code":"DRT2","Description":"","Local":false,"Name":"2-Piece","SortSeq":"02"},"DRT8":{"Code":"DRT8","Description":"","Local":false,"Name":"8-Piece","SortSeq":"05"},"9PC":{"Code":"9PC","Description":"","Local":false,"Name":"9-Piece","SortSeq":"06"}},"Drinks":{"2LTB":{"Code":"2LTB","Description":"","Local":false,"Name":"2-Liter Bottle","SortSeq":"01"},"20OZB":{"Code":"20OZB","Description":"","Local":false,"Name":"20oz Bottle","SortSeq":"02"}},"Pizza":{"10":{"Code":"10","Description":"","Local":false,"Name":"Small (10\")","SortSeq":"03"},"12":{"Code":"12","Description":"","Local":false,"Name":"Medium (12\")","SortSeq":"04"},"14":{"Code":"14","Description":"","Local":false,"Name":"Large (14\")","SortSeq":"05"},"16":{"Code":"16","Description":"","Local":true,"Name":"X-Large (16\")","SortSeq":"06"}},"Wings":{"8PCW":{"Code":"8PCW","Description":"","Local":false,"Name":"8-Piece","SortSeq":"12"},"14PCW":{"Code":"14PCW","Description":"","Local":false,"Name":"14-Piece","SortSeq":"13"},"40PCW":{"Code":"40PCW","Description":"","Local":false,"Name":"40-Piece","SortSeq":"14"},"12PCB":{"Code":"12PCB","Description":"","Local":false,"Name":"12-Piece Bites","SortSeq":"15"}}},"Toppings":{"Bread":{"K":{"Availability":[],"Code":"K","Description":"","Local":false,"Name":"Bacon","Tags":{"Meat":true}},"J":{"Availability":[],"Code":"J","Description":"","Local":false,"Name":"Jalapeno Peppers","Tags":{"Vege":true,"NonMeat":true}},"Si":{"Availability":[],"Code":"Si","Description":"","Local":false,"Name":"Spinach","Tags":{"Vege":true,"NonMeat":true}},"Fe":{"Availability":[],"Code":"Fe","Description":"","Local":false,"Name":"Feta Cheese","Tags":{"NonMeat":true}}},"Pasta":{"Xf":{"Availability":[],"Code":"Xf","Description":"","Local":false,"Name":"Alfredo Sauce","Tags":{"WholeOnly":true,"IgnoreQty":true,"ExclusiveGroup":"Sauce","Sauce":true,"NonMeat":true}},"Xm":{"Availability":[],"Code":"Xm","Description":"","Local":false,"Name":"Hearty Marinara Sauce","Tags":{"WholeOnly":true,"IgnoreQty":true,"ExclusiveGroup":"Sauce","Sauce":true,"NonMeat":true}},"P":{"Availability":[],"Code":"P","Description":"","Local":false,"Name":"Pepperoni","Tags":{"Meat":true}},"S":{"Availability":[],"Code":"S","Description":"","Local":false,"Name":"Italian Sausage","Tags":{"Meat":true}},"B":{"Availability":[],"Code":"B","Description":"","Local":false,"Name":"Beef","Tags":{"Meat":true}},"Pm":{"Availability":[],"Code":"Pm","Description":"","Local":false,"Name":"Philly Steak","Tags":{"Meat":true}},"H":{"Availability":[],"Code":"H","Description":"","Local":false,"Name":"Ham","Tags":{"Meat":true}},"K":{"Availability":[],"Code":"K","Description":"","Local":false,"Name":"Bacon","Tags":{"Meat":true}},"Du":{"Availability":[],"Code":"Du","Description":"","Local":false,"Name":"Premium Chicken","Tags":{"Meat":true}},"C":{"Availability":[],"Code":"C","Description":"","Local":false,"Name":"Cheese","Tags":{"Cheese":true,"NonMeat":true}},"E":{"Availability":[],"Code":"E","Description":"","Local":false,"Name":"Cheddar Cheese","Tags":{"Cheese":true,"NonMeat":true}},"Fe":{"Availability":[],"Code":"Fe","Description":"","Local":false,"Name":"Feta Cheese","Tags":{"Cheese":true,"NonMeat":true}},"Cs":{"Availability":[],"Code":"Cs","Description":"","Local":false,"Name":"Shredded Parmesan","Tags":{"Cheese":true,"NonMeat":true}},"Cp":{"Availability":[],"Code":"Cp","Description":"","Local":false,"Name":"Shredded Provolone Cheese","Tags":{"Cheese":true,"NonMeat":true}},"F":{"Availability":[],"Code":"F","Description":"","Local":true,"Name":"Garlic","Tags":{"Vege":true,"NonMeat":true}},"G":{"Availability":[],"Code":"G","Description":"","Local":false,"Name":"Green Peppers","Tags":{"Vege":true,"NonMeat":true}},"J":{"Availability":[],"Code":"J","Description":"","Local":false,"Name":"Jalapeno Peppers","Tags":{"Vege":true,"NonMeat":true}},"M":{"Availability":[],"Code":"M","Description":"","Local":false,"Name":"Mushrooms","Tags":{"Vege":true,"NonMeat":true}},"N":{"Availability":[],"Code":"N","Description":"","Local":false,"Name":"Pineapple","Tags":{"Vege":true,"NonMeat":true}},"O":{"Availability":[],"Code":"O","Description":"","Local":false,"Name":"Onions","Tags":{"Vege":true,"NonMeat":true}},"R":{"Availability":[],"Code":"R","Description":"","Local":false,"Name":"Black Olives","Tags":{"Vege":true,"NonMeat":true}},"Rr":{"Availability":[],"Code":"Rr","Description":"","Local":false,"Name":"Roasted Red Peppers","Tags":{"Vege":true,"NonMeat":true}},"Si":{"Availability":[],"Code":"Si","Description":"","Local":false,"Name":"Spinach","Tags":{"Vege":true,"NonMeat":true}},"Td":{"Availability":[],"Code":"Td","Description":"","Local":false,"Name":"Diced Tomatoes","Tags":{"Vege":true,"NonMeat":true}},"Z":{"Availability":[],"Code":"Z","Description":"","Local":false,"Name":"Banana Peppers","Tags":{"Vege":true,"NonMeat":true}}},"Pizza":{"X":{"Availability":[],"Code":"X","Description":"","Local":false,"Name":"Robust Inspired Tomato Sauce","Tags":{"WholeOnly":true,"IgnoreQty":true,"ExclusiveGroup":"Sauce","Sauce":true,"NonMeat":true}},"Xm":{"Availability":[],"Code":"Xm","Description":"","Local":false,"Name":"Hearty Marinara Sauce","Tags":{"WholeOnly":true,"IgnoreQty":true,"ExclusiveGroup":"Sauce","Sauce":true,"NonMeat":true}},"Bq":{"Availability":[],"Code":"Bq","Description":"","Local":false,"Name":"BBQ Sauce","Tags":{"WholeOnly":true,"IgnoreQty":true,"ExclusiveGroup":"Sauce","Sauce":true,"NonMeat":true}},"Xw":{"Availability":[],"Code":"Xw","Description":"","Local":false,"Name":"Garlic Parmesan White Sauce","Tags":{"WholeOnly":true,"IgnoreQty":true,"ExclusiveGroup":"Sauce","Sauce":true,"NonMeat":true}},"C":{"Availability":[],"Code":"C","Description":"","Local":false,"Name":"Cheese","Tags":{"Cheese":true,"NonMeat":true}},"H":{"Availability":[],"Code":"H","Description":"","Local":false,"Name":"Ham","Tags":{"Meat":true}},"B":{"Availability":[],"Code":"B","Description":"","Local":false,"Name":"Beef","Tags":{"Meat":true}},"Sa":{"Availability":[],"Code":"Sa","Description":"","Local":false,"Name":"Salami","Tags":{"Meat":true}},"P":{"Availability":[],"Code":"P","Description":"","Local":false,"Name":"Pepperoni","Tags":{"Meat":true}},"S":{"Availability":[],"Code":"S","Description":"","Local":false,"Name":"Italian Sausage","Tags":{"Meat":true}},"Du":{"Availability":[],"Code":"Du","Description":"","Local":false,"Name":"Premium Chicken","Tags":{"Meat":true}},"K":{"Availability":[],"Code":"K","Description":"","Local":false,"Name":"Bacon","Tags":{"Meat":true}},"Pm":{"Availability":[],"Code":"Pm","Description":"","Local":false,"Name":"Philly Steak","Tags":{"Meat":true}},"Ht":{"Availability":[],"Code":"Ht","Description":"","Local":false,"Name":"Hot Sauce","Tags":{"NonMeat":true}},"F":{"Availability":[],"Code":"F","Description":"","Local":true,"Name":"Garlic","Tags":{"Vege":true,"NonMeat":true}},"J":{"Availability":[],"Code":"J","Description":"","Local":false,"Name":"Jalapeno Peppers","Tags":{"Vege":true,"NonMeat":true}},"O":{"Availability":[],"Code":"O","Description":"","Local":false,"Name":"Onions","Tags":{"Vege":true,"NonMeat":true}},"Z":{"Availability":[],"Code":"Z","Description":"","Local":false,"Name":"Banana Peppers","Tags":{"Vege":true,"NonMeat":true}},"Td":{"Availability":[],"Code":"Td","Description":"","Local":false,"Name":"Diced Tomatoes","Tags":{"Vege":true,"NonMeat":true}},"R":{"Availability":[],"Code":"R","Description":"","Local":false,"Name":"Black Olives","Tags":{"Vege":true,"NonMeat":true}},"M":{"Availability":[],"Code":"M","Description":"","Local":false,"Name":"Mushrooms","Tags":{"Vege":true,"NonMeat":true}},"N":{"Availability":[],"Code":"N","Description":"","Local":false,"Name":"Pineapple","Tags":{"Vege":true,"NonMeat":true}},"Cp":{"Availability":[],"Code":"Cp","Description":"","Local":false,"Name":"Shredded Provolone Cheese","Tags":{"NonMeat":true,"BaseOptionQty":"1"}},"E":{"Availability":[],"Code":"E","Description":"","Local":false,"Name":"Cheddar Cheese","Tags":{"NonMeat":true}},"G":{"Availability":[],"Code":"G","Description":"","Local":false,"Name":"Green Peppers","Tags":{"Vege":true,"NonMeat":true}},"Si":{"Availability":[],"Code":"Si","Description":"","Local":false,"Name":"Spinach","Tags":{"Vege":true,"NonMeat":true}},"Rr":{"Availability":[],"Code":"Rr","Description":"","Local":false,"Name":"Roasted Red Peppers","Tags":{"Vege":true,"NonMeat":true}},"Fe":{"Availability":[],"Code":"Fe","Description":"","Local":false,"Name":"Feta Cheese","Tags":{"NonMeat":true}},"Cs":{"Availability":[],"Code":"Cs","Description":"","Local":false,"Name":"Shredded Parmesan Asiago","Tags":{"NonMeat":true}},"Ac":{"Availability":[],"Code":"Ac","Description":"","Local":false,"Name":"American Cheese","Tags":{"NonMeat":true}},"Xf":{"Availability":[],"Code":"Xf","Description":"","Local":false,"Name":"Alfredo Sauce","Tags":{"WholeOnly":true,"IgnoreQty":true,"ExclusiveGroup":"Sauce","Sauce":true,"NonMeat":true}}},"Sandwich":{"X":{"Availability":[],"Code":"X","Description":"","Local":false,"Name":"Pizza Sauce","Tags":{"WholeOnly":true,"IgnoreQty":true,"Sauce":true,"NonMeat":true}},"Mh":{"Availability":[],"Code":"Mh","Description":"","Local":false,"Name":"Mango Habanero Sauce","Tags":{"WholeOnly":true,"IgnoreQty":true,"Sauce":true,"NonMeat":true}},"Bd":{"Availability":[],"Code":"Bd","Description":"","Local":false,"Name":"Blue Cheese Sauce","Tags":{"WholeOnly":true,"IgnoreQty":true,"Sauce":true,"NonMeat":true}},"Rd":{"Availability":[],"Code":"Rd","Description":"","Local":false,"Name":"Ranch Dressing","Tags":{"WholeOnly":true,"IgnoreQty":true,"Sauce":true,"Vege":true,"NonMeat":true}},"Ht":{"Availability":[],"Code":"Ht","Description":"","Local":false,"Name":"Hot Sauce","Tags":{"WholeOnly":true,"IgnoreQty":true,"Sauce":true,"Vege":true,"NonMeat":true}},"C":{"Availability":[],"Code":"C","Description":"","Local":false,"Name":"Cheese","Tags":{"NonMeat":true}},"P":{"Availability":[],"Code":"P","Description":"","Local":false,"Name":"Pepperoni","Tags":{"Meat":true}},"Pm":{"Availability":[],"Code":"Pm","Description":"","Local":false,"Name":"Philly Steak","Tags":{"Meat":true}},"H":{"Availability":[],"Code":"H","Description":"","Local":false,"Name":"Ham","Tags":{"Meat":true}},"K":{"Availability":[],"Code":"K","Description":"","Local":false,"Name":"Bacon","Tags":{"Meat":true}},"Sa":{"Availability":[],"Code":"Sa","Description":"","Local":false,"Name":"Salami","Tags":{"Meat":true}},"Du":{"Availability":[],"Code":"Du","Description":"","Local":false,"Name":"Premium Chicken","Tags":{"Meat":true}},"Ac":{"Availability":[],"Code":"Ac","Description":"","Local":false,"Name":"American Cheese","Tags":{"NonMeat":true}},"E":{"Availability":[],"Code":"E","Description":"","Local":false,"Name":"Cheddar Cheese","Tags":{"NonMeat":true}},"Fe":{"Availability":[],"Code":"Fe","Description":"","Local":false,"Name":"Feta Cheese","Tags":{"NonMeat":true}},"Cs":{"Availability":[],"Code":"Cs","Description":"","Local":false,"Name":"Shredded Parmesan Asiago","Tags":{"NonMeat":true}},"Pv":{"Availability":[],"Code":"Pv","Description":"","Local":false,"Name":"Sliced Provolone","Tags":{"NonMeat":true}},"Z":{"Availability":[],"Code":"Z","Description":"","Local":false,"Name":"Banana Peppers","Tags":{"Vege":true,"NonMeat":true}},"G":{"Availability":[],"Code":"G","Description":"","Local":false,"Name":"Green Peppers","Tags":{"Vege":true,"NonMeat":true}},"J":{"Availability":[],"Code":"J","Description":"","Local":false,"Name":"Jalapeno Peppers","Tags":{"Vege":true,"NonMeat":true}},"M":{"Availability":[],"Code":"M","Description":"","Local":false,"Name":"Mushrooms","Tags":{"Vege":true,"NonMeat":true}},"N":{"Availability":[],"Code":"N","Description":"","Local":false,"Name":"Pineapple","Tags":{"Vege":true,"NonMeat":true}},"O":{"Availability":[],"Code":"O","Description":"","Local":false,"Name":"Onions","Tags":{"Vege":true,"NonMeat":true}},"Rr":{"Availability":[],"Code":"Rr","Description":"","Local":false,"Name":"Roasted Red Peppers","Tags":{"Vege":true,"NonMeat":true}},"Si":{"Availability":[],"Code":"Si","Description":"","Local":false,"Name":"Spinach","Tags":{"Vege":true,"NonMeat":true}},"Td":{"Availability":[],"Code":"Td","Description":"","Local":false,"Name":"Diced Tomatoes","Tags":{"Vege":true,"NonMeat":true}}},"Wings":{"K":{"Availability":[],"Code":"K","Description":"","Local":false,"Name":"Bacon","Tags":{"Meat":true,"Side":false}},"Td":{"Availability":[],"Code":"Td","Description":"","Local":false,"Name":"Diced Tomatoes","Tags":{"Vege":true,"NonMeat":true,"Side":false}},"J":{"Availability":[],"Code":"J","Description":"","Local":false,"Name":"Jalapeno Peppers","Tags":{"Vege":true,"NonMeat":true,"Side":false}},"N":{"Availability":[],"Code":"N","Description":"","Local":false,"Name":"Pineapple","Tags":{"Vege":true,"NonMeat":true,"Side":false}}}},"Variants":{"B8PCPT":{"Code":"B8PCPT","FlavorCode":"","ImageCode":"B8PCPT","Local":false,"Name":"Parmesan Bread Twists","Price":"6.99","ProductCode":"F_PARMT","SizeCode":"BRD8","Tags":{"BreadType":"Twists","DefaultSides":"SIDMAR=1","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"10.99","Price1-0":"6.99","Price1-3":"9.99","Price2-3":"9.99","Price1-4":"10.99","Price2-2":"8.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"8.99","Price2-0":"6.99"},"Surcharge":"0"},"B8PCGT":{"Code":"B8PCGT","FlavorCode":"","ImageCode":"B8PCGT","Local":false,"Name":"Garlic Bread Twists","Price":"6.99","ProductCode":"F_GARLICT","SizeCode":"BRD8","Tags":{"BreadType":"Twists","DefaultSides":"SIDMAR=1","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"10.99","Price1-0":"6.99","Price1-3":"9.99","Price2-3":"9.99","Price1-4":"10.99","Price2-2":"8.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"8.99","Price2-0":"6.99"},"Surcharge":"0"},"B8PCSCB":{"Code":"B8PCSCB","FlavorCode":"","ImageCode":"B8PCSCB","Local":false,"Name":"Stuffed Cheesy Bread","Price":"6.99","ProductCode":"F_SCBRD","SizeCode":"BRD8","Tags":{"BreadType":"Stuffed","DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"10.99","Price1-0":"6.99","Price1-3":"9.99","Price2-3":"9.99","Price1-4":"10.99","Price2-2":"8.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"8.99","Price2-0":"6.99"},"Surcharge":"0"},"B8PCSSF":{"Code":"B8PCSSF","FlavorCode":"","ImageCode":"B8PCSSF","Local":false,"Name":"Stuffed Cheesy Bread with Spinach & Feta","Price":"6.99","ProductCode":"F_SSBRD","SizeCode":"BRD8","Tags":{"BreadType":"Stuffed","DefaultSides":"","DefaultToppings":"Si=1,Fe=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"10.99","Price1-0":"6.99","Price1-3":"9.99","Price2-3":"9.99","Price1-4":"10.99","Price2-2":"8.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"8.99","Price2-0":"6.99"},"Surcharge":"0"},"B8PCSBJ":{"Code":"B8PCSBJ","FlavorCode":"","ImageCode":"B8PCSBJ","Local":false,"Name":"Stuffed Cheesy Bread with Bacon & Jalapeno","Price":"6.99","ProductCode":"F_SBBRD","SizeCode":"BRD8","Tags":{"BreadType":"Stuffed","DefaultSides":"","DefaultToppings":"K=1,J=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"10.99","Price1-0":"6.99","Price1-3":"9.99","Price2-3":"9.99","Price1-4":"10.99","Price2-2":"8.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"8.99","Price2-0":"6.99"},"Surcharge":"0"},"B16PBIT":{"Code":"B16PBIT","FlavorCode":"","ImageCode":"B16PBIT","Local":false,"Name":"16-Piece Parmesan Bread Bites","Price":"3.99","ProductCode":"F_PBITES","SizeCode":"BRD16","Tags":{"DefaultSides":"SIDMAR=1","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"3.99","Price1-0":"3.99","Price1-3":"3.99","Price2-3":"3.99","Price1-4":"3.99","Price2-2":"3.99","Price1-1":"3.99","Price2-1":"3.99","Price1-2":"3.99","Price2-0":"3.99"},"Surcharge":"0"},"B32PBIT":{"Code":"B32PBIT","FlavorCode":"","ImageCode":"B32PBIT","Local":false,"Name":"32-Piece Parmesan Bread Bites","Price":"5.99","ProductCode":"F_PBITES","SizeCode":"BRD32","Tags":{"DefaultSides":"SIDMAR=1","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"5.99","Price1-0":"5.99","Price1-3":"5.99","Price2-3":"5.99","Price1-4":"5.99","Price2-2":"5.99","Price1-1":"5.99","Price2-1":"5.99","Price1-2":"5.99","Price2-0":"5.99"},"Surcharge":"0"},"B8PCCT":{"Code":"B8PCCT","FlavorCode":"","ImageCode":"B8PCCT","Local":false,"Name":"Cinnamon Bread Twists","Price":"6.99","ProductCode":"F_CINNAT","SizeCode":"DRT8","Tags":{"BreadType":"Twists","DefaultSides":"SIDICE=1","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"10.99","Price1-0":"6.99","Price1-3":"9.99","Price2-3":"9.99","Price1-4":"10.99","Price2-2":"8.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"8.99","Price2-0":"6.99"},"Surcharge":"0"},"MARBRWNE":{"Code":"MARBRWNE","FlavorCode":"","ImageCode":"MARBRWNE","Local":false,"Name":"Domino's Marbled Cookie Brownie™","Price":"6.99","ProductCode":"F_MRBRWNE","SizeCode":"9PC","Tags":{"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"10.99","Price1-0":"6.99","Price1-3":"9.99","Price2-3":"9.99","Price1-4":"10.99","Price2-2":"8.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"8.99","Price2-0":"6.99"},"Surcharge":"0"},"B2PCLAVA":{"Code":"B2PCLAVA","FlavorCode":"","ImageCode":"B2PCLAVA","Local":false,"Name":"Chocolate Lava Crunch Cakes","Price":"4.99","ProductCode":"F_LAVA","SizeCode":"DRT2","Tags":{"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"7.99","Price1-0":"4.99","Price1-3":"7.24","Price2-3":"7.24","Price1-4":"7.99","Price2-2":"6.49","Price1-1":"5.74","Price2-1":"5.74","Price1-2":"6.49","Price2-0":"4.99"},"Surcharge":"0"},"20BCOKE":{"Code":"20BCOKE","FlavorCode":"","ImageCode":"20BCOKE","Local":false,"Name":"20oz Bottle Coke®","Price":"1.99","ProductCode":"F_COKE","SizeCode":"20OZB","Tags":{"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"1.99","Price1-0":"1.99","Price1-3":"1.99","Price2-3":"1.99","Price1-4":"1.99","Price2-2":"1.99","Price1-1":"1.99","Price2-1":"1.99","Price1-2":"1.99","Price2-0":"1.99"},"Surcharge":"0"},"20BORNG":{"Code":"20BORNG","FlavorCode":"","ImageCode":"20BORNG","Local":false,"Name":"20oz Bottle Fanta® Orange","Price":"1.99","ProductCode":"F_ORAN","SizeCode":"20OZB","Tags":{"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"1.99","Price1-0":"1.99","Price1-3":"1.99","Price2-3":"1.99","Price1-4":"1.99","Price2-2":"1.99","Price1-1":"1.99","Price2-1":"1.99","Price1-2":"1.99","Price2-0":"1.99"},"Surcharge":"0"},"20BSPRITE":{"Code":"20BSPRITE","FlavorCode":"","ImageCode":"20BSPRITE","Local":false,"Name":"20oz Bottle Sprite®","Price":"1.99","ProductCode":"F_SPRITE","SizeCode":"20OZB","Tags":{"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"1.99","Price1-0":"1.99","Price1-3":"1.99","Price2-3":"1.99","Price1-4":"1.99","Price2-2":"1.99","Price1-1":"1.99","Price2-1":"1.99","Price1-2":"1.99","Price2-0":"1.99"},"Surcharge":"0"},"2LCOKE":{"Code":"2LCOKE","FlavorCode":"","ImageCode":"2LCOKE","Local":false,"Name":"2-Liter Coke®","Price":"2.99","ProductCode":"F_COKE","SizeCode":"2LTB","Tags":{"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"2.99","Price1-0":"2.99","Price1-3":"2.99","Price2-3":"2.99","Price1-4":"2.99","Price2-2":"2.99","Price1-1":"2.99","Price2-1":"2.99","Price1-2":"2.99","Price2-0":"2.99"},"Surcharge":"0"},"2LDCOKE":{"Code":"2LDCOKE","FlavorCode":"","ImageCode":"2LDCOKE","Local":false,"Name":"2-Liter Diet Coke®","Price":"2.99","ProductCode":"F_DIET","SizeCode":"2LTB","Tags":{"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"2.99","Price1-0":"2.99","Price1-3":"2.99","Price2-3":"2.99","Price1-4":"2.99","Price2-2":"2.99","Price1-1":"2.99","Price2-1":"2.99","Price1-2":"2.99","Price2-0":"2.99"},"Surcharge":"0"},"20BDCOKE":{"Code":"20BDCOKE","FlavorCode":"","ImageCode":"20BDCOKE","Local":false,"Name":"20oz Bottle Diet Coke®","Price":"1.99","ProductCode":"F_DIET","SizeCode":"20OZB","Tags":{"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"1.99","Price1-0":"1.99","Price1-3":"1.99","Price2-3":"1.99","Price1-4":"1.99","Price2-2":"1.99","Price1-1":"1.99","Price2-1":"1.99","Price1-2":"1.99","Price2-0":"1.99"},"Surcharge":"0"},"2LMMORANGE":{"Code":"2LMMORANGE","FlavorCode":"","ImageCode":"2LMMORANGE","Local":true,"Name":"2-Liter Fanta® Orange","Price":"2.99","ProductCode":"F_ORAN","SizeCode":"2LTB","Tags":{"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"2.99","Price1-0":"2.99","Price1-3":"2.99","Price2-3":"2.99","Price1-4":"2.99","Price2-2":"2.99","Price1-1":"2.99","Price2-1":"2.99","Price1-2":"2.99","Price2-0":"2.99"},"Surcharge":"0"},"2LSPRITE":{"Code":"2LSPRITE","FlavorCode":"","ImageCode":"2LSPRITE","Local":false,"Name":"2-Liter Sprite®","Price":"2.99","ProductCode":"F_SPRITE","SizeCode":"2LTB","Tags":{"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"2.99","Price1-0":"2.99","Price1-3":"2.99","Price2-3":"2.99","Price1-4":"2.99","Price2-2":"2.99","Price1-1":"2.99","Price2-1":"2.99","Price1-2":"2.99","Price2-0":"2.99"},"Surcharge":"0"},"BOTTLWATER":{"Code":"BOTTLWATER","FlavorCode":"","ImageCode":"BOTTLWATER","Local":false,"Name":"20oz Dasani® Bottle Water","Price":"1.99","ProductCode":"F_WATER","SizeCode":"20OZB","Tags":{"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"1.99","Price1-0":"1.99","Price1-3":"1.99","Price2-3":"1.99","Price1-4":"1.99","Price2-2":"1.99","Price1-1":"1.99","Price2-1":"1.99","Price1-2":"1.99","Price2-0":"1.99"},"Surcharge":"0"},"D20BFITLEM":{"Code":"D20BFITLEM","FlavorCode":"","ImageCode":"D20BFITLEM","Local":true,"Name":"20-oz Bottle FUZE® Iced Tea Lemon","Price":"1.99","ProductCode":"F_FITLEM","SizeCode":"20OZB","Tags":{"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"1.99","Price1-0":"1.99","Price1-3":"1.99","Price2-3":"1.99","Price1-4":"1.99","Price2-2":"1.99","Price1-1":"1.99","Price2-1":"1.99","Price1-2":"1.99","Price2-0":"1.99"},"Surcharge":"0"},"PPSGARSA":{"Code":"PPSGARSA","FlavorCode":"","ImageCode":"PPSGARSA","Local":false,"Name":"Classic Garden","Price":"6.99","ProductCode":"F_GARDEN","SizeCode":"","Tags":{"DefaultSides":"RANCHPK=1","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"10.99","Price1-0":"6.99","Price1-3":"9.99","Price2-3":"9.99","Price1-4":"10.99","Price2-2":"8.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"8.99","Price2-0":"6.99"},"Surcharge":"0"},"PPSCSRSA":{"Code":"PPSCSRSA","FlavorCode":"","ImageCode":"PPSCSRSA","Local":false,"Name":"Chicken Caesar","Price":"6.99","ProductCode":"F_CCAESAR","SizeCode":"","Tags":{"DefaultSides":"CAESAR=1","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"10.99","Price1-0":"6.99","Price1-3":"9.99","Price2-3":"9.99","Price1-4":"10.99","Price2-2":"8.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"8.99","Price2-0":"6.99"},"Surcharge":"0"},"PINPASBD":{"Code":"PINPASBD","FlavorCode":"PASTA","ImageCode":"PINPASBD","Local":false,"Name":"Build Your Own Pasta","Price":"7.99","ProductCode":"S_BUILD","SizeCode":"","Tags":{"SauceRequired":true,"DefaultSides":"","DefaultToppings":"Xf=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"8.99","Price1-0":"7.99","Price1-3":"7.99","Price2-3":"7.99","Price1-4":"8.99","Price2-2":"7.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"7.99","Price2-0":"7.99"},"Surcharge":"0"},"PINPASCA":{"Code":"PINPASCA","FlavorCode":"PASTA","ImageCode":"PINPASCA","Local":false,"Name":"Chicken Alfredo Pasta","Price":"7.99","ProductCode":"S_ALFR","SizeCode":"","Tags":{"DefaultSides":"","DefaultToppings":"Du=1,Xf=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"11.99","Price1-0":"7.99","Price1-3":"10.99","Price2-3":"10.99","Price1-4":"11.99","Price2-2":"9.99","Price1-1":"8.99","Price2-1":"8.99","Price1-2":"9.99","Price2-0":"7.99"},"Surcharge":"0"},"PINPASCC":{"Code":"PINPASCC","FlavorCode":"PASTA","ImageCode":"PINPASCC","Local":false,"Name":"Chicken Carbonara Pasta","Price":"7.99","ProductCode":"S_CARB","SizeCode":"","Tags":{"MaxOptionQty":"4","DefaultSides":"","DefaultToppings":"M=1,O=1,Du=1,K=1,Xf=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"11.99","Price1-0":"7.99","Price1-3":"10.99","Price2-3":"10.99","Price1-4":"11.99","Price2-2":"9.99","Price1-1":"8.99","Price2-1":"8.99","Price1-2":"9.99","Price2-0":"7.99"},"Surcharge":"0"},"PINPASMM":{"Code":"PINPASMM","FlavorCode":"PASTA","ImageCode":"PINPASMM","Local":false,"Name":"Italian Sausage Marinara Pasta","Price":"7.99","ProductCode":"S_MARIN","SizeCode":"","Tags":{"DefaultSides":"","DefaultToppings":"S=1,Cp=1,Xm=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"11.99","Price1-0":"7.99","Price1-3":"10.99","Price2-3":"10.99","Price1-4":"11.99","Price2-2":"9.99","Price1-1":"8.99","Price2-1":"8.99","Price1-2":"9.99","Price2-0":"7.99"},"Surcharge":"0"},"PINPASPP":{"Code":"PINPASPP","FlavorCode":"PASTA","ImageCode":"PINPASPP","Local":false,"Name":"Pasta Primavera","Price":"7.99","ProductCode":"S_PRIM","SizeCode":"","Tags":{"MaxOptionQty":"4","DefaultSides":"","DefaultToppings":"M=1,O=1,Td=1,Si=1,Xf=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"11.99","Price1-0":"7.99","Price1-3":"10.99","Price2-3":"10.99","Price1-4":"11.99","Price2-2":"9.99","Price1-1":"8.99","Price2-1":"8.99","Price1-2":"9.99","Price2-0":"7.99"},"Surcharge":"0"},"PINBBLBD":{"Code":"PINBBLBD","FlavorCode":"BBOWL","ImageCode":"PINBBLBD","Local":false,"Name":"Build your Own BreadBowl Pasta","Price":"8.99","ProductCode":"S_BUILD","SizeCode":"","Tags":{"SauceRequired":true,"DefaultSides":"","DefaultToppings":"Xf=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"9.99","Price1-0":"8.99","Price1-3":"8.99","Price2-3":"8.99","Price1-4":"9.99","Price2-2":"8.99","Price1-1":"8.99","Price2-1":"8.99","Price1-2":"8.99","Price2-0":"8.99"},"Surcharge":"0"},"PINBBLCA":{"Code":"PINBBLCA","FlavorCode":"BBOWL","ImageCode":"PINBBLCA","Local":false,"Name":"Chicken Alfredo BreadBowl Pasta","Price":"8.99","ProductCode":"S_ALFR","SizeCode":"","Tags":{"DefaultSides":"","DefaultToppings":"Du=1,Xf=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"12.99","Price1-0":"8.99","Price1-3":"11.99","Price2-3":"11.99","Price1-4":"12.99","Price2-2":"10.99","Price1-1":"9.99","Price2-1":"9.99","Price1-2":"10.99","Price2-0":"8.99"},"Surcharge":"0"},"PINBBLCC":{"Code":"PINBBLCC","FlavorCode":"BBOWL","ImageCode":"PINBBLCC","Local":false,"Name":"Chicken Carbonara BreadBowl Pasta","Price":"8.99","ProductCode":"S_CARB","SizeCode":"","Tags":{"MaxOptionQty":"4","DefaultSides":"","DefaultToppings":"M=1,O=1,Du=1,K=1,Xf=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"12.99","Price1-0":"8.99","Price1-3":"11.99","Price2-3":"11.99","Price1-4":"12.99","Price2-2":"10.99","Price1-1":"9.99","Price2-1":"9.99","Price1-2":"10.99","Price2-0":"8.99"},"Surcharge":"0"},"PINBBLMM":{"Code":"PINBBLMM","FlavorCode":"BBOWL","ImageCode":"PINBBLMM","Local":false,"Name":"Italian Sausage Marinara BreadBowl Pasta","Price":"8.99","ProductCode":"S_MARIN","SizeCode":"","Tags":{"sodiumWarningEnabled":true,"DefaultSides":"","DefaultToppings":"S=1,Cp=1,Xm=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"12.99","Price1-0":"8.99","Price1-3":"11.99","Price2-3":"11.99","Price1-4":"12.99","Price2-2":"10.99","Price1-1":"9.99","Price2-1":"9.99","Price1-2":"10.99","Price2-0":"8.99"},"Surcharge":"0"},"PINBBLPP":{"Code":"PINBBLPP","FlavorCode":"BBOWL","ImageCode":"PINBBLPP","Local":false,"Name":"Pasta Primavera BreadBowl","Price":"8.99","ProductCode":"S_PRIM","SizeCode":"","Tags":{"MaxOptionQty":"4","DefaultSides":"","DefaultToppings":"M=1,O=1,Td=1,Si=1,Xf=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"12.99","Price1-0":"8.99","Price1-3":"11.99","Price2-3":"11.99","Price1-4":"12.99","Price2-2":"10.99","Price1-1":"9.99","Price2-1":"9.99","Price1-2":"10.99","Price2-0":"8.99"},"Surcharge":"0"},"10SCDELUX":{"Code":"10SCDELUX","FlavorCode":"HANDTOSS","ImageCode":"10SCDELUX","Local":false,"Name":"Small (10\") Hand Tossed Deluxe","Price":"12.99","ProductCode":"S_DX","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,P=1,M=1,O=1,G=1,S=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"10SCMEATZA":{"Code":"10SCMEATZA","FlavorCode":"HANDTOSS","ImageCode":"10SCMEATZA","Local":false,"Name":"Small (10\") Hand Tossed MeatZZa","Price":"12.99","ProductCode":"S_MX","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1.5,P=1,H=1,S=1,B=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"P10IREBP":{"Code":"P10IREBP","FlavorCode":"HANDTOSS","ImageCode":"P10IREBP","Local":false,"Name":"Small (10\") Hand Tossed Buffalo Chicken","Price":"12.99","ProductCode":"S_PIZBP","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"O=1,Du=1,E=1,Cp=1,Ac=1,Ht=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"P10IRECK":{"Code":"P10IRECK","FlavorCode":"HANDTOSS","ImageCode":"P10IRECK","Local":false,"Name":"Small (10\") Hand Tossed Memphis BBQ Chicken ","Price":"12.99","ProductCode":"S_PIZCK","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,Bq=1,O=1,Du=1,E=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"P10IRECR":{"Code":"P10IRECR","FlavorCode":"HANDTOSS","ImageCode":"P10IRECR","Local":false,"Name":"Small (10\") Hand Tossed Cali Chicken Bacon Ranch","Price":"12.99","ProductCode":"S_PIZCR","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,Xw=1,Du=1,K=1,Td=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"P10IRECZ":{"Code":"P10IRECZ","FlavorCode":"HANDTOSS","ImageCode":"P10IRECZ","Local":false,"Name":"Small (10\") Hand Tossed Wisconsin 6 Cheese Pizza","Price":"12.99","ProductCode":"S_PIZCZ","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,E=1,Fe=1,Cs=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"P10IREPH":{"Code":"P10IREPH","FlavorCode":"HANDTOSS","ImageCode":"P10IREPH","Local":false,"Name":"Small (10\") Hand Tossed Philly Cheese Steak","Price":"12.99","ProductCode":"S_PIZPH","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"M=1,O=1,G=1,Pm=1,Cp=1,Ac=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"P10IREPV":{"Code":"P10IREPV","FlavorCode":"HANDTOSS","ImageCode":"P10IREPV","Local":false,"Name":"Small (10\") Hand Tossed Pacific Veggie","Price":"12.99","ProductCode":"S_PIZPV","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,M=1,O=1,R=1,Td=1,Rr=1,Si=1,Fe=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"10SCPFEAST":{"Code":"10SCPFEAST","FlavorCode":"HANDTOSS","ImageCode":"10SCPFEAST","Local":false,"Name":"Small (10\") Hand Tossed Ultimate Pepperoni","Price":"12.99","ProductCode":"S_PIZPX","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,P=1.5,Cs=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"P10IREUH":{"Code":"P10IREUH","FlavorCode":"HANDTOSS","ImageCode":"P10IREUH","Local":false,"Name":"Small (10\") Hand Tossed Honolulu Hawaiian","Price":"12.99","ProductCode":"S_PIZUH","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,H=1,N=1,K=1,Rr=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"10SCREEN":{"Code":"10SCREEN","FlavorCode":"HANDTOSS","ImageCode":"10SCREEN","Local":false,"Name":"Small (10\") Hand Tossed Pizza","Price":"9.49","ProductCode":"S_PIZZA","SizeCode":"10","Tags":{"sodiumWarningEnabled":true,"DefaultSides":"","DefaultToppings":"X=1,C=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"15.05","Price1-0":"9.49","Price1-3":"13.66","Price2-3":"13.66","Price1-4":"15.05","Price2-2":"12.27","Price1-1":"10.88","Price2-1":"10.88","Price1-2":"12.27","Price2-0":"9.49"},"Surcharge":"0"},"10SCEXTRAV":{"Code":"10SCEXTRAV","FlavorCode":"HANDTOSS","ImageCode":"10SCEXTRAV","Local":false,"Name":"Small (10\") Hand Tossed ExtravaganZZa ","Price":"12.99","ProductCode":"S_ZZ","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1.5,P=1,H=1,M=1,O=1,G=1,R=1,S=1,B=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"10TDELUX":{"Code":"10TDELUX","FlavorCode":"THIN","ImageCode":"10TDELUX","Local":true,"Name":"Small (10\") Thin Deluxe","Price":"12.99","ProductCode":"S_DX","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,P=1,M=1,O=1,G=1,S=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"10TMEATZA":{"Code":"10TMEATZA","FlavorCode":"THIN","ImageCode":"10TMEATZA","Local":true,"Name":"Small (10\") Thin MeatZZa","Price":"12.99","ProductCode":"S_MX","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1.5,P=1,H=1,S=1,B=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"P10ITHBP":{"Code":"P10ITHBP","FlavorCode":"THIN","ImageCode":"P10ITHBP","Local":true,"Name":"Small (10\") Thin Crust Buffalo Chicken","Price":"12.99","ProductCode":"S_PIZBP","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"O=1,Du=1,E=1,Cp=1,Ac=1,Ht=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"P10ITHCK":{"Code":"P10ITHCK","FlavorCode":"THIN","ImageCode":"P10ITHCK","Local":true,"Name":"Small (10\") Thin Crust Memphis BBQ Chicken ","Price":"12.99","ProductCode":"S_PIZCK","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,Bq=1,O=1,Du=1,E=1,Cp=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"P10ITHCR":{"Code":"P10ITHCR","FlavorCode":"THIN","ImageCode":"P10ITHCR","Local":true,"Name":"Small (10\") Thin Crust Cali Chicken Bacon Ranch","Price":"12.99","ProductCode":"S_PIZCR","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,Xw=1,Du=1,K=1,Td=1,Cp=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"P10ITHCZ":{"Code":"P10ITHCZ","FlavorCode":"THIN","ImageCode":"P10ITHCZ","Local":true,"Name":"Small (10\") Thin Wisconsin 6 Cheese Pizza","Price":"12.99","ProductCode":"S_PIZCZ","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,E=1,Fe=1,Cs=1,Cp=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"P10ITHPH":{"Code":"P10ITHPH","FlavorCode":"THIN","ImageCode":"P10ITHPH","Local":true,"Name":"Small (10\") Thin Philly Cheese Steak","Price":"12.99","ProductCode":"S_PIZPH","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"M=1,O=1,G=1,Pm=1,Cp=1,Ac=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"P10ITHPV":{"Code":"P10ITHPV","FlavorCode":"THIN","ImageCode":"P10ITHPV","Local":true,"Name":"Small (10\") Thin Crust Pacific Veggie","Price":"12.99","ProductCode":"S_PIZPV","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,M=1,O=1,R=1,Td=1,Rr=1,Si=1,Fe=1,Cp=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"10TPFEAST":{"Code":"10TPFEAST","FlavorCode":"THIN","ImageCode":"10TPFEAST","Local":true,"Name":"Small (10\") Thin Ultimate Pepperoni","Price":"12.99","ProductCode":"S_PIZPX","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,P=1.5,Cs=1,Cp=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"P10ITHUH":{"Code":"P10ITHUH","FlavorCode":"THIN","ImageCode":"P10ITHUH","Local":true,"Name":"Small (10\") Thin Crust Honolulu Hawaiian","Price":"12.99","ProductCode":"S_PIZUH","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,H=1,N=1,K=1,Rr=1,Cp=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"10THIN":{"Code":"10THIN","FlavorCode":"THIN","ImageCode":"10THIN","Local":true,"Name":"Small (10\") Thin Pizza","Price":"9.49","ProductCode":"S_PIZZA","SizeCode":"10","Tags":{"sodiumWarningEnabled":true,"DefaultSides":"","DefaultToppings":"X=1,C=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"15.05","Price1-0":"9.49","Price1-3":"13.66","Price2-3":"13.66","Price1-4":"15.05","Price2-2":"12.27","Price1-1":"10.88","Price2-1":"10.88","Price1-2":"12.27","Price2-0":"9.49"},"Surcharge":"0"},"10TEXTRAV":{"Code":"10TEXTRAV","FlavorCode":"THIN","ImageCode":"10TEXTRAV","Local":true,"Name":"Small (10\") Thin ExtravaganZZa ","Price":"12.99","ProductCode":"S_ZZ","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1.5,P=1,H=1,M=1,O=1,G=1,R=1,S=1,B=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"12SCDELUX":{"Code":"12SCDELUX","FlavorCode":"HANDTOSS","ImageCode":"12SCDELUX","Local":false,"Name":"Medium (12\") Hand Tossed Deluxe","Price":"14.99","ProductCode":"S_DX","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,P=1,M=1,O=1,G=1,S=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"12SCMEATZA":{"Code":"12SCMEATZA","FlavorCode":"HANDTOSS","ImageCode":"12SCMEATZA","Local":false,"Name":"Medium (12\") Hand Tossed MeatZZa","Price":"14.99","ProductCode":"S_MX","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1.5,P=1,H=1,S=1,B=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"P12IREBP":{"Code":"P12IREBP","FlavorCode":"HANDTOSS","ImageCode":"P12IREBP","Local":false,"Name":"Medium (12\") Hand Tossed Buffalo Chicken","Price":"14.99","ProductCode":"S_PIZBP","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"O=1,Du=1,E=1,Cp=1,Ac=1,Ht=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"P12IRECK":{"Code":"P12IRECK","FlavorCode":"HANDTOSS","ImageCode":"P12IRECK","Local":false,"Name":"Medium (12\") Hand Tossed Memphis BBQ Chicken ","Price":"14.99","ProductCode":"S_PIZCK","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,Bq=1,O=1,Du=1,E=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"P12IRECR":{"Code":"P12IRECR","FlavorCode":"HANDTOSS","ImageCode":"P12IRECR","Local":false,"Name":"Medium (12\") Hand Tossed Cali Chicken Bacon Ranch","Price":"14.99","ProductCode":"S_PIZCR","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,Xw=1,Du=1,K=1,Td=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"P12IRECZ":{"Code":"P12IRECZ","FlavorCode":"HANDTOSS","ImageCode":"P12IRECZ","Local":false,"Name":"Medium (12\") Hand Tossed Wisconsin 6 Cheese Pizza","Price":"14.99","ProductCode":"S_PIZCZ","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,E=1,Fe=1,Cs=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"P12IREPH":{"Code":"P12IREPH","FlavorCode":"HANDTOSS","ImageCode":"P12IREPH","Local":false,"Name":"Medium (12\") Hand Tossed Philly Cheese Steak","Price":"14.99","ProductCode":"S_PIZPH","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"M=1,O=1,G=1,Pm=1,Cp=1,Ac=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"P12IREPV":{"Code":"P12IREPV","FlavorCode":"HANDTOSS","ImageCode":"P12IREPV","Local":false,"Name":"Medium (12\") Hand Tossed Pacific Veggie","Price":"14.99","ProductCode":"S_PIZPV","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,M=1,O=1,R=1,Td=1,Rr=1,Si=1,Fe=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"12SCPFEAST":{"Code":"12SCPFEAST","FlavorCode":"HANDTOSS","ImageCode":"12SCPFEAST","Local":false,"Name":"Medium (12\") Hand Tossed Ultimate Pepperoni","Price":"14.99","ProductCode":"S_PIZPX","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,P=1.5,Cs=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"P12IREUH":{"Code":"P12IREUH","FlavorCode":"HANDTOSS","ImageCode":"P12IREUH","Local":false,"Name":"Medium (12\") Hand Tossed Honolulu Hawaiian","Price":"14.99","ProductCode":"S_PIZUH","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,H=1,N=1,K=1,Rr=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"12SCREEN":{"Code":"12SCREEN","FlavorCode":"HANDTOSS","ImageCode":"12SCREEN","Local":false,"Name":"Medium (12\") Hand Tossed Pizza","Price":"11.99","ProductCode":"S_PIZZA","SizeCode":"12","Tags":{"sodiumWarningEnabled":true,"DefaultSides":"","DefaultToppings":"X=1,C=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"18.35","Price1-0":"11.99","Price1-3":"16.76","Price2-3":"16.76","Price1-4":"18.35","Price2-2":"15.17","Price1-1":"13.58","Price2-1":"13.58","Price1-2":"15.17","Price2-0":"11.99"},"Surcharge":"0"},"12SCEXTRAV":{"Code":"12SCEXTRAV","FlavorCode":"HANDTOSS","ImageCode":"12SCEXTRAV","Local":false,"Name":"Medium (12\") Hand Tossed ExtravaganZZa ","Price":"14.99","ProductCode":"S_ZZ","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1.5,P=1,H=1,M=1,O=1,G=1,R=1,S=1,B=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"12TDELUX":{"Code":"12TDELUX","FlavorCode":"THIN","ImageCode":"12TDELUX","Local":false,"Name":"Medium (12\") Thin Deluxe","Price":"14.99","ProductCode":"S_DX","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,P=1,M=1,O=1,G=1,S=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"12TMEATZA":{"Code":"12TMEATZA","FlavorCode":"THIN","ImageCode":"12TMEATZA","Local":false,"Name":"Medium (12\") Thin MeatZZa","Price":"14.99","ProductCode":"S_MX","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1.5,P=1,H=1,S=1,B=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"P12ITHBP":{"Code":"P12ITHBP","FlavorCode":"THIN","ImageCode":"P12ITHBP","Local":false,"Name":"Medium (12\") Thin Crust Buffalo Chicken","Price":"14.99","ProductCode":"S_PIZBP","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"O=1,Du=1,E=1,Cp=1,Ac=1,Ht=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"P12ITHCK":{"Code":"P12ITHCK","FlavorCode":"THIN","ImageCode":"P12ITHCK","Local":false,"Name":"Medium (12\") Thin Crust Memphis BBQ Chicken ","Price":"14.99","ProductCode":"S_PIZCK","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,Bq=1,O=1,Du=1,E=1,Cp=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"P12ITHCR":{"Code":"P12ITHCR","FlavorCode":"THIN","ImageCode":"P12ITHCR","Local":false,"Name":"Medium (12\") Thin Crust Cali Chicken Bacon Ranch","Price":"14.99","ProductCode":"S_PIZCR","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,Xw=1,Du=1,K=1,Td=1,Cp=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"P12ITHCZ":{"Code":"P12ITHCZ","FlavorCode":"THIN","ImageCode":"P12ITHCZ","Local":false,"Name":"Medium (12\") Thin Wisconsin 6 Cheese Pizza","Price":"14.99","ProductCode":"S_PIZCZ","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,E=1,Fe=1,Cs=1,Cp=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"P12ITHPH":{"Code":"P12ITHPH","FlavorCode":"THIN","ImageCode":"P12ITHPH","Local":false,"Name":"Medium (12\") Thin Philly Cheese Steak","Price":"14.99","ProductCode":"S_PIZPH","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"M=1,O=1,G=1,Pm=1,Cp=1,Ac=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"P12ITHPV":{"Code":"P12ITHPV","FlavorCode":"THIN","ImageCode":"P12ITHPV","Local":false,"Name":"Medium (12\") Thin Crust Pacific Veggie","Price":"14.99","ProductCode":"S_PIZPV","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,M=1,O=1,R=1,Td=1,Rr=1,Si=1,Fe=1,Cp=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"12TPFEAST":{"Code":"12TPFEAST","FlavorCode":"THIN","ImageCode":"12TPFEAST","Local":false,"Name":"Medium (12\") Thin Ultimate Pepperoni","Price":"14.99","ProductCode":"S_PIZPX","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,P=1.5,Cs=1,Cp=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"P12ITHUH":{"Code":"P12ITHUH","FlavorCode":"THIN","ImageCode":"P12ITHUH","Local":false,"Name":"Medium (12\") Thin Crust Honolulu Hawaiian","Price":"14.99","ProductCode":"S_PIZUH","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,H=1,N=1,K=1,Rr=1,Cp=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"12THIN":{"Code":"12THIN","FlavorCode":"THIN","ImageCode":"12THIN","Local":false,"Name":"Medium (12\") Thin Pizza","Price":"11.99","ProductCode":"S_PIZZA","SizeCode":"12","Tags":{"sodiumWarningEnabled":true,"DefaultSides":"","DefaultToppings":"X=1,C=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"18.35","Price1-0":"11.99","Price1-3":"16.76","Price2-3":"16.76","Price1-4":"18.35","Price2-2":"15.17","Price1-1":"13.58","Price2-1":"13.58","Price1-2":"15.17","Price2-0":"11.99"},"Surcharge":"0"},"12TEXTRAV":{"Code":"12TEXTRAV","FlavorCode":"THIN","ImageCode":"12TEXTRAV","Local":false,"Name":"Medium (12\") Thin ExtravaganZZa ","Price":"14.99","ProductCode":"S_ZZ","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1.5,P=1,H=1,M=1,O=1,G=1,R=1,S=1,B=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"PBKIREDX":{"Code":"PBKIREDX","FlavorCode":"BK","ImageCode":"PBKIREDX","Local":false,"Name":"Large (14\") Brooklyn Deluxe","Price":"17.99","ProductCode":"S_DX","SizeCode":"14","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1,P=1,M=1,O=1,G=1,S=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"PBKIREMX":{"Code":"PBKIREMX","FlavorCode":"BK","ImageCode":"PBKIREMX","Local":false,"Name":"Large (14\") Brooklyn MeatZZa","Price":"17.99","ProductCode":"S_MX","SizeCode":"14","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1.5,P=1,H=1,S=1,B=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14IBKBP":{"Code":"P14IBKBP","FlavorCode":"BK","ImageCode":"P14IBKBP","Local":false,"Name":"Large (14\") Brooklyn Buffalo Chicken","Price":"17.99","ProductCode":"S_PIZBP","SizeCode":"14","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"O=1,Du=1,E=1,Cp=1,Ac=1,Ht=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14IBKCK":{"Code":"P14IBKCK","FlavorCode":"BK","ImageCode":"P14IBKCK","Local":false,"Name":"Large (14\") Brooklyn Memphis BBQ Chicken ","Price":"17.99","ProductCode":"S_PIZCK","SizeCode":"14","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"C=1,Bq=1,O=1,Du=1,E=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14IBKCR":{"Code":"P14IBKCR","FlavorCode":"BK","ImageCode":"P14IBKCR","Local":false,"Name":"Large (14\") Brooklyn Cali Chicken Bacon Ranch","Price":"17.99","ProductCode":"S_PIZCR","SizeCode":"14","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"C=1,Xw=1,Du=1,K=1,Td=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14IBKCZ":{"Code":"P14IBKCZ","FlavorCode":"BK","ImageCode":"P14IBKCZ","Local":false,"Name":"Large (14\") Brooklyn Wisconsin 6 Cheese Pizza","Price":"17.99","ProductCode":"S_PIZCZ","SizeCode":"14","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1,E=1,Fe=1,Cs=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14IBKPH":{"Code":"P14IBKPH","FlavorCode":"BK","ImageCode":"P14IBKPH","Local":false,"Name":"Large (14\") Brooklyn Philly Cheese Steak","Price":"17.99","ProductCode":"S_PIZPH","SizeCode":"14","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"M=1,O=1,G=1,Pm=1,Cp=1,Ac=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14IBKPV":{"Code":"P14IBKPV","FlavorCode":"BK","ImageCode":"P14IBKPV","Local":false,"Name":"Large (14\") Brooklyn Pacific Veggie","Price":"17.99","ProductCode":"S_PIZPV","SizeCode":"14","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1,M=1,O=1,R=1,Td=1,Rr=1,Si=1,Fe=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"PBKIREPX":{"Code":"PBKIREPX","FlavorCode":"BK","ImageCode":"PBKIREPX","Local":false,"Name":"Large (14\") Brooklyn Ultimate Pepperoni","Price":"17.99","ProductCode":"S_PIZPX","SizeCode":"14","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1,P=1.5,Cs=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14IBKUH":{"Code":"P14IBKUH","FlavorCode":"BK","ImageCode":"P14IBKUH","Local":false,"Name":"Large (14\") Brooklyn Honolulu Hawaiian","Price":"17.99","ProductCode":"S_PIZUH","SizeCode":"14","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1,H=1,N=1,K=1,Rr=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"PBKIREZA":{"Code":"PBKIREZA","FlavorCode":"BK","ImageCode":"PBKIREZA","Local":false,"Name":"Large (14\") Brooklyn Pizza","Price":"13.99","ProductCode":"S_PIZZA","SizeCode":"14","Tags":{"sodiumWarningEnabled":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"21.15","Price1-0":"13.99","Price1-3":"19.36","Price2-3":"19.36","Price1-4":"21.15","Price2-2":"17.57","Price1-1":"15.78","Price2-1":"15.78","Price1-2":"17.57","Price2-0":"13.99"},"Surcharge":"0"},"PBKIREZZ":{"Code":"PBKIREZZ","FlavorCode":"BK","ImageCode":"PBKIREZZ","Local":false,"Name":"Large (14\") Brooklyn ExtravaganZZa ","Price":"17.99","ProductCode":"S_ZZ","SizeCode":"14","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1.5,P=1,H=1,M=1,O=1,G=1,R=1,S=1,B=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"14SCDELUX":{"Code":"14SCDELUX","FlavorCode":"HANDTOSS","ImageCode":"14SCDELUX","Local":false,"Name":"Large (14\") Hand Tossed Deluxe","Price":"17.99","ProductCode":"S_DX","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,P=1,M=1,O=1,G=1,S=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"14SCMEATZA":{"Code":"14SCMEATZA","FlavorCode":"HANDTOSS","ImageCode":"14SCMEATZA","Local":false,"Name":"Large (14\") Hand Tossed MeatZZa","Price":"17.99","ProductCode":"S_MX","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1.5,P=1,H=1,S=1,B=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14IREBP":{"Code":"P14IREBP","FlavorCode":"HANDTOSS","ImageCode":"P14IREBP","Local":false,"Name":"Large (14\") Hand Tossed Buffalo Chicken","Price":"17.99","ProductCode":"S_PIZBP","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"O=1,Du=1,E=1,Cp=1,Ac=1,Ht=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14IRECK":{"Code":"P14IRECK","FlavorCode":"HANDTOSS","ImageCode":"P14IRECK","Local":false,"Name":"Large (14\") Hand Tossed Memphis BBQ Chicken ","Price":"17.99","ProductCode":"S_PIZCK","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,Bq=1,O=1,Du=1,E=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14IRECR":{"Code":"P14IRECR","FlavorCode":"HANDTOSS","ImageCode":"P14IRECR","Local":false,"Name":"Large (14\") Hand Tossed Cali Chicken Bacon Ranch","Price":"17.99","ProductCode":"S_PIZCR","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,Xw=1,Du=1,K=1,Td=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14IRECZ":{"Code":"P14IRECZ","FlavorCode":"HANDTOSS","ImageCode":"P14IRECZ","Local":false,"Name":"Large (14\") Hand Tossed Wisconsin 6 Cheese Pizza","Price":"17.99","ProductCode":"S_PIZCZ","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,E=1,Fe=1,Cs=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14IREPH":{"Code":"P14IREPH","FlavorCode":"HANDTOSS","ImageCode":"P14IREPH","Local":false,"Name":"Large (14\") Hand Tossed Philly Cheese Steak","Price":"17.99","ProductCode":"S_PIZPH","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"M=1,O=1,G=1,Pm=1,Cp=1,Ac=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14IREPV":{"Code":"P14IREPV","FlavorCode":"HANDTOSS","ImageCode":"P14IREPV","Local":false,"Name":"Large (14\") Hand Tossed Pacific Veggie","Price":"17.99","ProductCode":"S_PIZPV","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,M=1,O=1,R=1,Td=1,Rr=1,Si=1,Fe=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"14SCPFEAST":{"Code":"14SCPFEAST","FlavorCode":"HANDTOSS","ImageCode":"14SCPFEAST","Local":false,"Name":"Large (14\") Hand Tossed Ultimate Pepperoni","Price":"17.99","ProductCode":"S_PIZPX","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,P=1.5,Cs=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14IREUH":{"Code":"P14IREUH","FlavorCode":"HANDTOSS","ImageCode":"P14IREUH","Local":false,"Name":"Large (14\") Hand Tossed Honolulu Hawaiian","Price":"17.99","ProductCode":"S_PIZUH","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,H=1,N=1,K=1,Rr=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"14SCREEN":{"Code":"14SCREEN","FlavorCode":"HANDTOSS","ImageCode":"14SCREEN","Local":false,"Name":"Large (14\") Hand Tossed Pizza","Price":"13.99","ProductCode":"S_PIZZA","SizeCode":"14","Tags":{"sodiumWarningEnabled":true,"DefaultSides":"","DefaultToppings":"X=1,C=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"21.15","Price1-0":"13.99","Price1-3":"19.36","Price2-3":"19.36","Price1-4":"21.15","Price2-2":"17.57","Price1-1":"15.78","Price2-1":"15.78","Price1-2":"17.57","Price2-0":"13.99"},"Surcharge":"0"},"14SCEXTRAV":{"Code":"14SCEXTRAV","FlavorCode":"HANDTOSS","ImageCode":"14SCEXTRAV","Local":false,"Name":"Large (14\") Hand Tossed ExtravaganZZa ","Price":"17.99","ProductCode":"S_ZZ","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1.5,P=1,H=1,M=1,O=1,G=1,R=1,S=1,B=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"14TDELUX":{"Code":"14TDELUX","FlavorCode":"THIN","ImageCode":"14TDELUX","Local":false,"Name":"Large (14\") Thin Deluxe","Price":"17.99","ProductCode":"S_DX","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,P=1,M=1,O=1,G=1,S=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"14TMEATZA":{"Code":"14TMEATZA","FlavorCode":"THIN","ImageCode":"14TMEATZA","Local":false,"Name":"Large (14\") Thin MeatZZa","Price":"17.99","ProductCode":"S_MX","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1.5,P=1,H=1,S=1,B=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14ITHBP":{"Code":"P14ITHBP","FlavorCode":"THIN","ImageCode":"P14ITHBP","Local":false,"Name":"Large (14\") Thin Crust Buffalo Chicken","Price":"17.99","ProductCode":"S_PIZBP","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"O=1,Du=1,E=1,Cp=1,Ac=1,Ht=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14ITHCK":{"Code":"P14ITHCK","FlavorCode":"THIN","ImageCode":"P14ITHCK","Local":false,"Name":"Large (14\") Thin Crust Memphis BBQ Chicken ","Price":"17.99","ProductCode":"S_PIZCK","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,Bq=1,O=1,Du=1,E=1,Cp=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14ITHCR":{"Code":"P14ITHCR","FlavorCode":"THIN","ImageCode":"P14ITHCR","Local":false,"Name":"Large (14\") Thin Crust Cali Chicken Bacon Ranch","Price":"17.99","ProductCode":"S_PIZCR","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,Xw=1,Du=1,K=1,Td=1,Cp=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14ITHCZ":{"Code":"P14ITHCZ","FlavorCode":"THIN","ImageCode":"P14ITHCZ","Local":false,"Name":"Large (14\") Thin Wisconsin 6 Cheese Pizza","Price":"17.99","ProductCode":"S_PIZCZ","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,E=1,Fe=1,Cs=1,Cp=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14ITHPH":{"Code":"P14ITHPH","FlavorCode":"THIN","ImageCode":"P14ITHPH","Local":false,"Name":"Large (14\") Thin Philly Cheese Steak","Price":"17.99","ProductCode":"S_PIZPH","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"M=1,O=1,G=1,Pm=1,Cp=1,Ac=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14ITHPV":{"Code":"P14ITHPV","FlavorCode":"THIN","ImageCode":"P14ITHPV","Local":false,"Name":"Large (14\") Thin Crust Pacific Veggie","Price":"17.99","ProductCode":"S_PIZPV","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,M=1,O=1,R=1,Td=1,Rr=1,Si=1,Fe=1,Cp=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"14TPFEAST":{"Code":"14TPFEAST","FlavorCode":"THIN","ImageCode":"14TPFEAST","Local":false,"Name":"Large (14\") Thin Ultimate Pepperoni","Price":"17.99","ProductCode":"S_PIZPX","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,P=1.5,Cs=1,Cp=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14ITHUH":{"Code":"P14ITHUH","FlavorCode":"THIN","ImageCode":"P14ITHUH","Local":false,"Name":"Large (14\") Thin Crust Honolulu Hawaiian","Price":"17.99","ProductCode":"S_PIZUH","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,H=1,N=1,K=1,Rr=1,Cp=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"14THIN":{"Code":"14THIN","FlavorCode":"THIN","ImageCode":"14THIN","Local":false,"Name":"Large (14\") Thin Pizza","Price":"13.99","ProductCode":"S_PIZZA","SizeCode":"14","Tags":{"sodiumWarningEnabled":true,"DefaultSides":"","DefaultToppings":"X=1,C=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"21.15","Price1-0":"13.99","Price1-3":"19.36","Price2-3":"19.36","Price1-4":"21.15","Price2-2":"17.57","Price1-1":"15.78","Price2-1":"15.78","Price1-2":"17.57","Price2-0":"13.99"},"Surcharge":"0"},"14TEXTRAV":{"Code":"14TEXTRAV","FlavorCode":"THIN","ImageCode":"14TEXTRAV","Local":false,"Name":"Large (14\") Thin ExtravaganZZa ","Price":"17.99","ProductCode":"S_ZZ","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1.5,P=1,H=1,M=1,O=1,G=1,R=1,S=1,B=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P16IBKDX":{"Code":"P16IBKDX","FlavorCode":"BK","ImageCode":"P16IBKDX","Local":true,"Name":"X-Large (16\") Brooklyn Deluxe","Price":"19.99","ProductCode":"S_DX","SizeCode":"16","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1,P=1,M=1,O=1,G=1,S=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"27.95","Price1-0":"19.99","Price1-3":"25.96","Price2-3":"25.96","Price1-4":"27.95","Price2-2":"23.97","Price1-1":"21.98","Price2-1":"21.98","Price1-2":"23.97","Price2-0":"19.99"},"Surcharge":"0"},"P16IBKMX":{"Code":"P16IBKMX","FlavorCode":"BK","ImageCode":"P16IBKMX","Local":true,"Name":"X-Large (16\") Brooklyn MeatZZa","Price":"19.99","ProductCode":"S_MX","SizeCode":"16","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1.5,P=1,H=1,S=1,B=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"27.95","Price1-0":"19.99","Price1-3":"25.96","Price2-3":"25.96","Price1-4":"27.95","Price2-2":"23.97","Price1-1":"21.98","Price2-1":"21.98","Price1-2":"23.97","Price2-0":"19.99"},"Surcharge":"0"},"P16IBKBP":{"Code":"P16IBKBP","FlavorCode":"BK","ImageCode":"P16IBKBP","Local":true,"Name":"X-Large (16\") Brooklyn Buffalo Chicken","Price":"19.99","ProductCode":"S_PIZBP","SizeCode":"16","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"O=1,Du=1,E=1,Cp=1,Ac=1,Ht=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"27.95","Price1-0":"19.99","Price1-3":"25.96","Price2-3":"25.96","Price1-4":"27.95","Price2-2":"23.97","Price1-1":"21.98","Price2-1":"21.98","Price1-2":"23.97","Price2-0":"19.99"},"Surcharge":"0"},"P16IBKCK":{"Code":"P16IBKCK","FlavorCode":"BK","ImageCode":"P16IBKCK","Local":true,"Name":"X-Large (16\") Brooklyn Memphis BBQ Chicken ","Price":"19.99","ProductCode":"S_PIZCK","SizeCode":"16","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"C=1,Bq=1,O=1,Du=1,E=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"27.95","Price1-0":"19.99","Price1-3":"25.96","Price2-3":"25.96","Price1-4":"27.95","Price2-2":"23.97","Price1-1":"21.98","Price2-1":"21.98","Price1-2":"23.97","Price2-0":"19.99"},"Surcharge":"0"},"P16IBKCR":{"Code":"P16IBKCR","FlavorCode":"BK","ImageCode":"P16IBKCR","Local":true,"Name":"X-Large (16\") Brooklyn Cali Chicken Bacon Ranch","Price":"19.99","ProductCode":"S_PIZCR","SizeCode":"16","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"C=1,Xw=1,Du=1,K=1,Td=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"27.95","Price1-0":"19.99","Price1-3":"25.96","Price2-3":"25.96","Price1-4":"27.95","Price2-2":"23.97","Price1-1":"21.98","Price2-1":"21.98","Price1-2":"23.97","Price2-0":"19.99"},"Surcharge":"0"},"P16IBKCZ":{"Code":"P16IBKCZ","FlavorCode":"BK","ImageCode":"P16IBKCZ","Local":true,"Name":"X-Large (16\") Brooklyn Wisconsin 6 Cheese Pizza","Price":"19.99","ProductCode":"S_PIZCZ","SizeCode":"16","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1,E=1,Fe=1,Cs=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"27.95","Price1-0":"19.99","Price1-3":"25.96","Price2-3":"25.96","Price1-4":"27.95","Price2-2":"23.97","Price1-1":"21.98","Price2-1":"21.98","Price1-2":"23.97","Price2-0":"19.99"},"Surcharge":"0"},"P16IBKPH":{"Code":"P16IBKPH","FlavorCode":"BK","ImageCode":"P16IBKPH","Local":true,"Name":"X-Large (16\") Brooklyn Philly Cheese Steak","Price":"19.99","ProductCode":"S_PIZPH","SizeCode":"16","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"M=1,O=1,G=1,Pm=1,Cp=1,Ac=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"27.95","Price1-0":"19.99","Price1-3":"25.96","Price2-3":"25.96","Price1-4":"27.95","Price2-2":"23.97","Price1-1":"21.98","Price2-1":"21.98","Price1-2":"23.97","Price2-0":"19.99"},"Surcharge":"0"},"P16IBKPV":{"Code":"P16IBKPV","FlavorCode":"BK","ImageCode":"P16IBKPV","Local":true,"Name":"X-Large (16\") Brooklyn Pacific Veggie","Price":"19.99","ProductCode":"S_PIZPV","SizeCode":"16","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1,M=1,O=1,R=1,Td=1,Rr=1,Si=1,Fe=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"27.95","Price1-0":"19.99","Price1-3":"25.96","Price2-3":"25.96","Price1-4":"27.95","Price2-2":"23.97","Price1-1":"21.98","Price2-1":"21.98","Price1-2":"23.97","Price2-0":"19.99"},"Surcharge":"0"},"P16IBKPX":{"Code":"P16IBKPX","FlavorCode":"BK","ImageCode":"P16IBKPX","Local":true,"Name":"X-Large (16\") Brooklyn Ultimate Pepperoni","Price":"19.99","ProductCode":"S_PIZPX","SizeCode":"16","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1,P=1.5,Cs=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"27.95","Price1-0":"19.99","Price1-3":"25.96","Price2-3":"25.96","Price1-4":"27.95","Price2-2":"23.97","Price1-1":"21.98","Price2-1":"21.98","Price1-2":"23.97","Price2-0":"19.99"},"Surcharge":"0"},"P16IBKUH":{"Code":"P16IBKUH","FlavorCode":"BK","ImageCode":"P16IBKUH","Local":true,"Name":"X-Large (16\") Brooklyn Honolulu Hawaiian","Price":"19.99","ProductCode":"S_PIZUH","SizeCode":"16","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1,H=1,N=1,K=1,Rr=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"27.95","Price1-0":"19.99","Price1-3":"25.96","Price2-3":"25.96","Price1-4":"27.95","Price2-2":"23.97","Price1-1":"21.98","Price2-1":"21.98","Price1-2":"23.97","Price2-0":"19.99"},"Surcharge":"0"},"P16IBKZA":{"Code":"P16IBKZA","FlavorCode":"BK","ImageCode":"P16IBKZA","Local":true,"Name":"X-Large (16\") Brooklyn Pizza","Price":"15.49","ProductCode":"S_PIZZA","SizeCode":"16","Tags":{"sodiumWarningEnabled":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"23.45","Price1-0":"15.49","Price1-3":"21.46","Price2-3":"21.46","Price1-4":"23.45","Price2-2":"19.47","Price1-1":"17.48","Price2-1":"17.48","Price1-2":"19.47","Price2-0":"15.49"},"Surcharge":"0"},"P16IBKZZ":{"Code":"P16IBKZZ","FlavorCode":"BK","ImageCode":"P16IBKZZ","Local":true,"Name":"X-Large (16\") Brooklyn ExtravaganZZa ","Price":"19.99","ProductCode":"S_ZZ","SizeCode":"16","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1.5,P=1,H=1,M=1,O=1,G=1,R=1,S=1,B=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"27.95","Price1-0":"19.99","Price1-3":"25.96","Price2-3":"25.96","Price1-4":"27.95","Price2-2":"23.97","Price1-1":"21.98","Price2-1":"21.98","Price1-2":"23.97","Price2-0":"19.99"},"Surcharge":"0"},"P10IGFDX":{"Code":"P10IGFDX","FlavorCode":"GLUTENF","ImageCode":"P10IGFDX","Local":false,"Name":"Small (10\") Gluten Free Crust Deluxe","Price":"12.99","ProductCode":"S_DX","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,P=1,M=1,O=1,G=1,S=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"3"},"P10IGFMX":{"Code":"P10IGFMX","FlavorCode":"GLUTENF","ImageCode":"P10IGFMX","Local":false,"Name":"Small (10\") Gluten Free Crust MeatZZa","Price":"12.99","ProductCode":"S_MX","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1.5,P=1,H=1,S=1,B=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"3"},"P10IGFBP":{"Code":"P10IGFBP","FlavorCode":"GLUTENF","ImageCode":"P10IGFBP","Local":false,"Name":"Small (10\") Gluten Free Crust Buffalo Chicken","Price":"12.99","ProductCode":"S_PIZBP","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"O=1,Du=1,E=1,Cp=1,Ac=1,Ht=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"3"},"P10IGFCK":{"Code":"P10IGFCK","FlavorCode":"GLUTENF","ImageCode":"P10IGFCK","Local":false,"Name":"Small (10\") Gluten Free Crust Memphis BBQ Chicken ","Price":"12.99","ProductCode":"S_PIZCK","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,Bq=1,O=1,Du=1,E=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"3"},"P10IGFCR":{"Code":"P10IGFCR","FlavorCode":"GLUTENF","ImageCode":"P10IGFCR","Local":false,"Name":"Small (10\") Gluten Free Crust Cali Chicken Bacon Ranch","Price":"12.99","ProductCode":"S_PIZCR","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,Xw=1,Du=1,K=1,Td=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"3"},"P10IGFCZ":{"Code":"P10IGFCZ","FlavorCode":"GLUTENF","ImageCode":"P10IGFCZ","Local":false,"Name":"Small (10\") Gluten Free Crust Wisconsin 6 Cheese Pizza","Price":"12.99","ProductCode":"S_PIZCZ","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,E=1,Fe=1,Cs=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"3"},"P10IGFPH":{"Code":"P10IGFPH","FlavorCode":"GLUTENF","ImageCode":"P10IGFPH","Local":false,"Name":"Small (10\") Gluten Free Crust Philly Cheese Steak","Price":"12.99","ProductCode":"S_PIZPH","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"M=1,O=1,G=1,Pm=1,Cp=1,Ac=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"3"},"P10IGFPV":{"Code":"P10IGFPV","FlavorCode":"GLUTENF","ImageCode":"P10IGFPV","Local":false,"Name":"Small (10\") Gluten Free Crust Pacific Veggie","Price":"12.99","ProductCode":"S_PIZPV","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,M=1,O=1,R=1,Td=1,Rr=1,Si=1,Fe=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"3"},"P10IGFPX":{"Code":"P10IGFPX","FlavorCode":"GLUTENF","ImageCode":"P10IGFPX","Local":false,"Name":"Small (10\") Gluten Free Crust Ultimate Pepperoni","Price":"12.99","ProductCode":"S_PIZPX","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,P=1.5,Cs=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"3"},"P10IGFUH":{"Code":"P10IGFUH","FlavorCode":"GLUTENF","ImageCode":"P10IGFUH","Local":false,"Name":"Small (10\") Gluten Free Crust Honolulu Hawaiian","Price":"12.99","ProductCode":"S_PIZUH","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,H=1,N=1,K=1,Rr=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"3"},"P10IGFZA":{"Code":"P10IGFZA","FlavorCode":"GLUTENF","ImageCode":"P10IGFZA","Local":false,"Name":"Small (10\") Gluten Free Crust Pizza","Price":"9.49","ProductCode":"S_PIZZA","SizeCode":"10","Tags":{"sodiumWarningEnabled":true,"DefaultSides":"","DefaultToppings":"X=1,C=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"15.05","Price1-0":"9.49","Price1-3":"13.66","Price2-3":"13.66","Price1-4":"15.05","Price2-2":"12.27","Price1-1":"10.88","Price2-1":"10.88","Price1-2":"12.27","Price2-0":"9.49"},"Surcharge":"3"},"P10IGFZZ":{"Code":"P10IGFZZ","FlavorCode":"GLUTENF","ImageCode":"P10IGFZZ","Local":false,"Name":"Small (10\") Gluten Free Crust ExtravaganZZa ","Price":"12.99","ProductCode":"S_ZZ","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"X=1,C=1.5,P=1,H=1,M=1,O=1,G=1,R=1,S=1,B=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"3"},"P12IPADX":{"Code":"P12IPADX","FlavorCode":"NPAN","ImageCode":"P12IPADX","Local":false,"Name":"Medium (12\") Handmade Pan Deluxe","Price":"14.99","ProductCode":"S_DX","SizeCode":"12","Tags":{"HideOption":"Cp","Specialty":true,"Promotion":"PAN","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1,P=1,M=1,O=1,G=1,S=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"1.5"},"P12IPAMX":{"Code":"P12IPAMX","FlavorCode":"NPAN","ImageCode":"P12IPAMX","Local":false,"Name":"Medium (12\") Handmade Pan MeatZZa","Price":"14.99","ProductCode":"S_MX","SizeCode":"12","Tags":{"HideOption":"Cp","Specialty":true,"Promotion":"PAN","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1.5,P=1,H=1,S=1,B=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"1.5"},"P12IPABP":{"Code":"P12IPABP","FlavorCode":"NPAN","ImageCode":"P12IPABP","Local":false,"Name":"Medium (12\") Handmade Pan Buffalo Chicken","Price":"14.99","ProductCode":"S_PIZBP","SizeCode":"12","Tags":{"HideOption":"Cp","Specialty":true,"Promotion":"PAN","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"O=1,Du=1,E=1,Cp=1,Ac=1,Ht=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"1.5"},"P12IPACK":{"Code":"P12IPACK","FlavorCode":"NPAN","ImageCode":"P12IPACK","Local":false,"Name":"Medium (12\") Handmade Pan Memphis BBQ Chicken ","Price":"14.99","ProductCode":"S_PIZCK","SizeCode":"12","Tags":{"HideOption":"Cp","Specialty":true,"Promotion":"PAN","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"C=1,Bq=1,O=1,Du=1,E=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"1.5"},"P12IPACR":{"Code":"P12IPACR","FlavorCode":"NPAN","ImageCode":"P12IPACR","Local":false,"Name":"Medium (12\") Handmade Pan Cali Chicken Bacon Ranch","Price":"14.99","ProductCode":"S_PIZCR","SizeCode":"12","Tags":{"HideOption":"Cp","Specialty":true,"Promotion":"PAN","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"C=1,Xw=1,Du=1,K=1,Td=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"1.5"},"P12IPACZ":{"Code":"P12IPACZ","FlavorCode":"NPAN","ImageCode":"P12IPACZ","Local":false,"Name":"Medium (12\") Handmade Pan Wisconsin 6 Cheese Pizza","Price":"14.99","ProductCode":"S_PIZCZ","SizeCode":"12","Tags":{"HideOption":"Cp","Specialty":true,"Promotion":"PAN","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1,E=1,Fe=1,Cs=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"1.5"},"P12IPAPH":{"Code":"P12IPAPH","FlavorCode":"NPAN","ImageCode":"P12IPAPH","Local":false,"Name":"Medium (12\") Handmade Pan Philly Cheese Steak","Price":"14.99","ProductCode":"S_PIZPH","SizeCode":"12","Tags":{"HideOption":"Cp","Specialty":true,"Promotion":"PAN","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"M=1,O=1,G=1,Pm=1,Cp=1,Ac=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"1.5"},"P12IPAPV":{"Code":"P12IPAPV","FlavorCode":"NPAN","ImageCode":"P12IPAPV","Local":false,"Name":"Medium (12\") Handmade Pan Pacific Veggie","Price":"14.99","ProductCode":"S_PIZPV","SizeCode":"12","Tags":{"HideOption":"Cp","Specialty":true,"Promotion":"PAN","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1,M=1,O=1,R=1,Td=1,Rr=1,Si=1,Fe=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"1.5"},"P12IPAPX":{"Code":"P12IPAPX","FlavorCode":"NPAN","ImageCode":"P12IPAPX","Local":false,"Name":"Medium (12\") Handmade Pan Ultimate Pepperoni","Price":"14.99","ProductCode":"S_PIZPX","SizeCode":"12","Tags":{"HideOption":"Cp","Specialty":true,"Promotion":"PAN","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1,P=1.5,Cs=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"1.5"},"P12IPAUH":{"Code":"P12IPAUH","FlavorCode":"NPAN","ImageCode":"P12IPAUH","Local":false,"Name":"Medium (12\") Handmade Pan Honolulu Hawaiian","Price":"14.99","ProductCode":"S_PIZUH","SizeCode":"12","Tags":{"HideOption":"Cp","Specialty":true,"Promotion":"PAN","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1,H=1,N=1,K=1,Rr=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"1.5"},"P12IPAZA":{"Code":"P12IPAZA","FlavorCode":"NPAN","ImageCode":"P12IPAZA","Local":false,"Name":"Medium (12\") Handmade Pan Pizza","Price":"11.99","ProductCode":"S_PIZZA","SizeCode":"12","Tags":{"HideOption":"Cp","WarnAfterOptionQty":"5","Promotion":"PAN","DisabledToppings":"C","sodiumWarningEnabled":true,"DefaultSides":"","DefaultToppings":"X=1,C=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"18.35","Price1-0":"11.99","Price1-3":"16.76","Price2-3":"16.76","Price1-4":"18.35","Price2-2":"15.17","Price1-1":"13.58","Price2-1":"13.58","Price1-2":"15.17","Price2-0":"11.99"},"Surcharge":"1.5"},"P12IPAZZ":{"Code":"P12IPAZZ","FlavorCode":"NPAN","ImageCode":"P12IPAZZ","Local":false,"Name":"Medium (12\") Handmade Pan ExtravaganZZa ","Price":"14.99","ProductCode":"S_ZZ","SizeCode":"12","Tags":{"HideOption":"Cp","Specialty":true,"Promotion":"PAN","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"X=1,C=1.5,P=1,H=1,M=1,O=1,G=1,R=1,S=1,B=1,Cp=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"1.5"},"P10IRESPF":{"Code":"P10IRESPF","FlavorCode":"HANDTOSS","ImageCode":"P10IRESPF","Local":false,"Name":"Small (10\") Hand Tossed Spinach & Feta","Price":"12.99","ProductCode":"S_PISPF","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,O=1,Si=1,Fe=1,Cs=1,Cp=1,Xf=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"P10ITHSPF":{"Code":"P10ITHSPF","FlavorCode":"THIN","ImageCode":"P10ITHSPF","Local":true,"Name":"Small (10\") Thin Spinach & Feta","Price":"12.99","ProductCode":"S_PISPF","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,O=1,Si=1,Fe=1,Cs=1,Cp=1,Xf=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"0"},"P10IGFSPF":{"Code":"P10IGFSPF","FlavorCode":"GLUTENF","ImageCode":"P10IGFSPF","Local":false,"Name":"Small (10\") Gluten Free Crust Spinach & Feta","Price":"12.99","ProductCode":"S_PISPF","SizeCode":"10","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,O=1,Si=1,Fe=1,Cs=1,Cp=1,Xf=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"18.55","Price1-0":"12.99","Price1-3":"17.16","Price2-3":"17.16","Price1-4":"18.55","Price2-2":"15.77","Price1-1":"14.38","Price2-1":"14.38","Price1-2":"15.77","Price2-0":"12.99"},"Surcharge":"3"},"P12IRESPF":{"Code":"P12IRESPF","FlavorCode":"HANDTOSS","ImageCode":"P12IRESPF","Local":false,"Name":"Medium (12\") Hand Tossed Spinach & Feta","Price":"14.99","ProductCode":"S_PISPF","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,O=1,Si=1,Fe=1,Cs=1,Cp=1,Xf=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"P12ITHSPF":{"Code":"P12ITHSPF","FlavorCode":"THIN","ImageCode":"P12ITHSPF","Local":false,"Name":"Medium (12\") Thin Spinach & Feta","Price":"14.99","ProductCode":"S_PISPF","SizeCode":"12","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,O=1,Si=1,Fe=1,Cs=1,Cp=1,Xf=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"0"},"P12IPASPF":{"Code":"P12IPASPF","FlavorCode":"NPAN","ImageCode":"P12IPASPF","Local":false,"Name":"Medium (12\") Handmade Pan Spinach & Feta","Price":"14.99","ProductCode":"S_PISPF","SizeCode":"12","Tags":{"Specialty":true,"Promotion":"PAN","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"C=1,O=1,Si=1,Fe=1,Cs=1,Cp=1,Xf=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"21.35","Price1-0":"14.99","Price1-3":"19.76","Price2-3":"19.76","Price1-4":"21.35","Price2-2":"18.17","Price1-1":"16.58","Price2-1":"16.58","Price1-2":"18.17","Price2-0":"14.99"},"Surcharge":"1.5"},"P14IBKSPF":{"Code":"P14IBKSPF","FlavorCode":"BK","ImageCode":"P14IBKSPF","Local":false,"Name":"Large (14\") Brooklyn Spinach & Feta","Price":"17.99","ProductCode":"S_PISPF","SizeCode":"14","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"C=1,O=1,Si=1,Fe=1,Cs=1,Cp=1,Xf=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14IRESPF":{"Code":"P14IRESPF","FlavorCode":"HANDTOSS","ImageCode":"P14IRESPF","Local":false,"Name":"Large (14\") Hand Tossed Spinach & Feta","Price":"17.99","ProductCode":"S_PISPF","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,O=1,Si=1,Fe=1,Cs=1,Cp=1,Xf=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT,GO,NGO","DefaultCookingInstructions":"NB,PIECT,GO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P14ITHSPF":{"Code":"P14ITHSPF","FlavorCode":"THIN","ImageCode":"P14ITHSPF","Local":false,"Name":"Large (14\") Thin Spinach & Feta","Price":"17.99","ProductCode":"S_PISPF","SizeCode":"14","Tags":{"Specialty":true,"DefaultSides":"","DefaultToppings":"C=1,O=1,Si=1,Fe=1,Cs=1,Cp=1,Xf=1"},"AllowedCookingInstructions":"PIECT,SQCT,UNCT,RGO,NOOR","DefaultCookingInstructions":"SQCT,RGO","Prepared":true,"Pricing":{"Price2-4":"25.15","Price1-0":"17.99","Price1-3":"23.36","Price2-3":"23.36","Price1-4":"25.15","Price2-2":"21.57","Price1-1":"19.78","Price2-1":"19.78","Price1-2":"21.57","Price2-0":"17.99"},"Surcharge":"0"},"P16IBKSPF":{"Code":"P16IBKSPF","FlavorCode":"BK","ImageCode":"P16IBKSPF","Local":true,"Name":"X-Large (16\") Brooklyn Spinach & Feta","Price":"19.99","ProductCode":"S_PISPF","SizeCode":"16","Tags":{"Specialty":true,"HideOption":"Cp","DisabledToppings":"C","DefaultSides":"","DefaultToppings":"C=1,O=1,Si=1,Fe=1,Cs=1,Cp=1,Xf=1"},"AllowedCookingInstructions":"WD,NB,PIECT,SQCT,UNCT","DefaultCookingInstructions":"NB,PIECT","Prepared":true,"Pricing":{"Price2-4":"27.95","Price1-0":"19.99","Price1-3":"25.96","Price2-3":"25.96","Price1-4":"27.95","Price2-2":"23.97","Price1-1":"21.98","Price2-1":"21.98","Price1-2":"23.97","Price2-0":"19.99"},"Surcharge":"0"},"PSANSACB":{"Code":"PSANSACB","FlavorCode":"","ImageCode":"PSANSACB","Local":false,"Name":"Chicken Bacon Ranch Sandwich","Price":"6.99","ProductCode":"S_CHIKK","SizeCode":"","Tags":{"DefaultSides":"","DefaultToppings":"C=1,Du=1,K=1,Pv=1,Rd=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"10.99","Price1-0":"6.99","Price1-3":"9.99","Price2-3":"9.99","Price1-4":"10.99","Price2-2":"8.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"8.99","Price2-0":"6.99"},"Surcharge":"0"},"PSANSACP":{"Code":"PSANSACP","FlavorCode":"","ImageCode":"PSANSACP","Local":false,"Name":"Chicken Parm Sandwich","Price":"6.99","ProductCode":"S_CHIKP","SizeCode":"","Tags":{"DefaultSides":"","DefaultToppings":"X=1,C=1,Du=1,Cs=1,Pv=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"10.99","Price1-0":"6.99","Price1-3":"9.99","Price2-3":"9.99","Price1-4":"10.99","Price2-2":"8.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"8.99","Price2-0":"6.99"},"Surcharge":"0"},"PSANSAIT":{"Code":"PSANSAIT","FlavorCode":"","ImageCode":"PSANSAIT","Local":false,"Name":"Italian Sandwich","Price":"6.99","ProductCode":"S_ITAL","SizeCode":"","Tags":{"DefaultSides":"","DefaultToppings":"C=1,P=1,H=1,O=1,G=1,Z=1,Sa=1,Pv=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"10.99","Price1-0":"6.99","Price1-3":"9.99","Price2-3":"9.99","Price1-4":"10.99","Price2-2":"8.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"8.99","Price2-0":"6.99"},"Surcharge":"0"},"PSANSAPH":{"Code":"PSANSAPH","FlavorCode":"","ImageCode":"PSANSAPH","Local":false,"Name":"Philly Cheese Steak","Price":"6.99","ProductCode":"S_PHIL","SizeCode":"","Tags":{"DefaultSides":"","DefaultToppings":"M=1,O=1,G=1,Pm=1,Ac=1,Pv=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"10.99","Price1-0":"6.99","Price1-3":"9.99","Price2-3":"9.99","Price1-4":"10.99","Price2-2":"8.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"8.99","Price2-0":"6.99"},"Surcharge":"0"},"PSANSABC":{"Code":"PSANSABC","FlavorCode":"","ImageCode":"PSANSABC","Local":false,"Name":"Buffalo Chicken Sandwich","Price":"6.99","ProductCode":"S_BUFC","SizeCode":"","Tags":{"DefaultSides":"","DefaultToppings":"C=1,O=1,Du=1,E=1,Ht=1,Pv=1,Bd=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"10.99","Price1-0":"6.99","Price1-3":"9.99","Price2-3":"9.99","Price1-4":"10.99","Price2-2":"8.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"8.99","Price2-0":"6.99"},"Surcharge":"0"},"PSANSACH":{"Code":"PSANSACH","FlavorCode":"","ImageCode":"PSANSACH","Local":false,"Name":"Chicken Habanero Sandwich","Price":"6.99","ProductCode":"S_CHHB","SizeCode":"","Tags":{"DefaultSides":"","DefaultToppings":"C=1,Du=1,N=1,E=1,J=1,Pv=1,Mh=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"10.99","Price1-0":"6.99","Price1-3":"9.99","Price2-3":"9.99","Price1-4":"10.99","Price2-2":"8.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"8.99","Price2-0":"6.99"},"Surcharge":"0"},"PSANSAMV":{"Code":"PSANSAMV","FlavorCode":"","ImageCode":"PSANSAMV","Local":false,"Name":"Mediterranean Veggie Sandwich","Price":"6.99","ProductCode":"S_MEDV","SizeCode":"","Tags":{"DefaultSides":"","DefaultToppings":"O=1,Td=1,Rr=1,Si=1,Fe=1,Ac=1,Z=1,Pv=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"10.99","Price1-0":"6.99","Price1-3":"9.99","Price2-3":"9.99","Price1-4":"10.99","Price2-2":"8.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"8.99","Price2-0":"6.99"},"Surcharge":"0"},"SIDEJAL":{"Code":"SIDEJAL","FlavorCode":"","ImageCode":"SIDEJAL","Local":true,"Name":"Side Jalapenos","Price":"0.5","ProductCode":"F_SIDJAL","SizeCode":"","Tags":{"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"0.5","Price1-0":"0.5","Price1-3":"0.5","Price2-3":"0.5","Price1-4":"0.5","Price2-2":"0.5","Price1-1":"0.5","Price2-1":"0.5","Price1-2":"0.5","Price2-0":"0.5"},"Surcharge":"0"},"PARMCHEESE":{"Code":"PARMCHEESE","FlavorCode":"","ImageCode":"PARMCHEESE","Local":true,"Name":"Parmesan Cheese Packets","Price":"0","ProductCode":"F_SIDPAR","SizeCode":"","Tags":{"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"0","Price1-0":"0","Price1-3":"0","Price2-3":"0","Price1-4":"0","Price2-2":"0","Price1-1":"0","Price2-1":"0","Price1-2":"0","Price2-0":"0"},"Surcharge":"0"},"REDPEPPER":{"Code":"REDPEPPER","FlavorCode":"","ImageCode":"REDPEPPER","Local":true,"Name":"Red Pepper Packets","Price":"0","ProductCode":"F_SIDRED","SizeCode":"","Tags":{"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"0","Price1-0":"0","Price1-3":"0","Price2-3":"0","Price1-4":"0","Price2-2":"0","Price1-1":"0","Price2-1":"0","Price1-2":"0","Price2-0":"0"},"Surcharge":"0"},"AGCAESAR":{"Code":"AGCAESAR","FlavorCode":"","ImageCode":"AGCAESAR","Local":false,"Name":"Caesar Dressing","Price":"0.75","ProductCode":"F_CAESAR","SizeCode":"","Tags":{"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"0.75","Price1-0":"0.75","Price1-3":"0.75","Price2-3":"0.75","Price1-4":"0.75","Price2-2":"0.75","Price1-1":"0.75","Price2-1":"0.75","Price1-2":"0.75","Price2-0":"0.75"},"Surcharge":"0"},"AGITAL":{"Code":"AGITAL","FlavorCode":"","ImageCode":"AGITAL","Local":true,"Name":"Italian Dressing","Price":"0.75","ProductCode":"F_ITAL","SizeCode":"","Tags":{"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"0.75","Price1-0":"0.75","Price1-3":"0.75","Price2-3":"0.75","Price1-4":"0.75","Price2-2":"0.75","Price1-1":"0.75","Price2-1":"0.75","Price1-2":"0.75","Price2-0":"0.75"},"Surcharge":"0"},"AGRANCH":{"Code":"AGRANCH","FlavorCode":"","ImageCode":"AGRANCH","Local":false,"Name":"Ranch Dressing","Price":"0.75","ProductCode":"F_RANCHPK","SizeCode":"","Tags":{"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"0.75","Price1-0":"0.75","Price1-3":"0.75","Price2-3":"0.75","Price1-4":"0.75","Price2-2":"0.75","Price1-1":"0.75","Price2-1":"0.75","Price1-2":"0.75","Price2-0":"0.75"},"Surcharge":"0"},"HOTSAUCE":{"Code":"HOTSAUCE","FlavorCode":"","ImageCode":"HOTSAUCE","Local":false,"Name":"Kicker Hot Dipping Cup","Price":"0.75","ProductCode":"F_HOTCUP","SizeCode":"","Tags":{"BONELESS":true,"BONEIN":true,"SideType":"DippingCup","DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"0.75","Price1-0":"0.75","Price1-3":"0.75","Price2-3":"0.75","Price1-4":"0.75","Price2-2":"0.75","Price1-1":"0.75","Price2-1":"0.75","Price1-2":"0.75","Price2-0":"0.75"},"Surcharge":"0"},"CEAHABC":{"Code":"CEAHABC","FlavorCode":"","ImageCode":"CEAHABC","Local":false,"Name":"Sweet Mango Habanero Dipping Cup","Price":"0.75","ProductCode":"F_SMHAB","SizeCode":"","Tags":{"EffectiveOn":"2010-01-01","BONELESS":true,"BONEIN":true,"SideType":"DippingCup","DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"0.75","Price1-0":"0.75","Price1-3":"0.75","Price2-3":"0.75","Price1-4":"0.75","Price2-2":"0.75","Price1-1":"0.75","Price2-1":"0.75","Price1-2":"0.75","Price2-0":"0.75"},"Surcharge":"0"},"CEABBQC":{"Code":"CEABBQC","FlavorCode":"","ImageCode":"CEABBQC","Local":false,"Name":"BBQ Dipping Cup","Price":"0.75","ProductCode":"F_BBQC","SizeCode":"","Tags":{"BONELESS":true,"BONEIN":true,"SideType":"DippingCup","DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"0.75","Price1-0":"0.75","Price1-3":"0.75","Price2-3":"0.75","Price1-4":"0.75","Price2-2":"0.75","Price1-1":"0.75","Price2-1":"0.75","Price1-2":"0.75","Price2-0":"0.75"},"Surcharge":"0"},"RANCH":{"Code":"RANCH","FlavorCode":"","ImageCode":"RANCH","Local":false,"Name":"Ranch Dipping Cup","Price":"0.75","ProductCode":"F_SIDRAN","SizeCode":"","Tags":{"BONELESS":true,"BONEIN":true,"SideType":"DippingCup","DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"0.75","Price1-0":"0.75","Price1-3":"0.75","Price2-3":"0.75","Price1-4":"0.75","Price2-2":"0.75","Price1-1":"0.75","Price2-1":"0.75","Price1-2":"0.75","Price2-0":"0.75"},"Surcharge":"0"},"BLUECHS":{"Code":"BLUECHS","FlavorCode":"","ImageCode":"BLUECHS","Local":false,"Name":"Blue Cheese Dipping Cup","Price":"0.75","ProductCode":"F_Bd","SizeCode":"","Tags":{"BONELESS":true,"BONEIN":true,"SideType":"DippingCup","DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"0.75","Price1-0":"0.75","Price1-3":"0.75","Price2-3":"0.75","Price1-4":"0.75","Price2-2":"0.75","Price1-1":"0.75","Price2-1":"0.75","Price1-2":"0.75","Price2-0":"0.75"},"Surcharge":"0"},"GARBUTTER":{"Code":"GARBUTTER","FlavorCode":"","ImageCode":"GARBUTTER","Local":false,"Name":"Garlic Dipping Cup","Price":"0.75","ProductCode":"F_SIDGAR","SizeCode":"","Tags":{"SideType":"DippingCup","DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"0.75","Price1-0":"0.75","Price1-3":"0.75","Price2-3":"0.75","Price1-4":"0.75","Price2-2":"0.75","Price1-1":"0.75","Price2-1":"0.75","Price1-2":"0.75","Price2-0":"0.75"},"Surcharge":"0"},"ICING":{"Code":"ICING","FlavorCode":"","ImageCode":"ICING","Local":false,"Name":"Icing Dipping Cup","Price":"0.75","ProductCode":"F_SIDICE","SizeCode":"","Tags":{"SideType":"DippingCup","DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"0.75","Price1-0":"0.75","Price1-3":"0.75","Price2-3":"0.75","Price1-4":"0.75","Price2-2":"0.75","Price1-1":"0.75","Price2-1":"0.75","Price1-2":"0.75","Price2-0":"0.75"},"Surcharge":"0"},"MARINARA":{"Code":"MARINARA","FlavorCode":"","ImageCode":"MARINARA","Local":false,"Name":"Marinara Sauce Dipping Cup","Price":"0.75","ProductCode":"F_SIDMAR","SizeCode":"","Tags":{"SideType":"DippingCup","DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"0.75","Price1-0":"0.75","Price1-3":"0.75","Price2-3":"0.75","Price1-4":"0.75","Price2-2":"0.75","Price1-1":"0.75","Price2-1":"0.75","Price1-2":"0.75","Price2-0":"0.75"},"Surcharge":"0"},"STJUDE":{"Code":"STJUDE","FlavorCode":"","ImageCode":"STJUDE","Local":false,"Name":"St. Jude Donation","Price":"1","ProductCode":"F_STJUDE","SizeCode":"","Tags":{"Donation":"STJUDE","ExcludeFromLoyalty":true,"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"1","Price1-0":"1","Price1-3":"1","Price2-3":"1","Price1-4":"1","Price2-2":"1","Price1-1":"1","Price2-1":"1","Price1-2":"1","Price2-0":"1"},"Surcharge":"0"},"STJUDE2":{"Code":"STJUDE2","FlavorCode":"","ImageCode":"STJUDE2","Local":false,"Name":"St. Jude Donation","Price":"2","ProductCode":"F_STJUDE","SizeCode":"","Tags":{"Donation":"STJUDE","ExcludeFromLoyalty":true,"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"2","Price1-0":"2","Price1-3":"2","Price2-3":"2","Price1-4":"2","Price2-2":"2","Price1-1":"2","Price2-1":"2","Price1-2":"2","Price2-0":"2"},"Surcharge":"0"},"STJUDE5":{"Code":"STJUDE5","FlavorCode":"","ImageCode":"STJUDE5","Local":false,"Name":"St. Jude Donation","Price":"5","ProductCode":"F_STJUDE","SizeCode":"","Tags":{"Donation":"STJUDE","ExcludeFromLoyalty":true,"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"5","Price1-0":"5","Price1-3":"5","Price2-3":"5","Price1-4":"5","Price2-2":"5","Price1-1":"5","Price2-1":"5","Price1-2":"5","Price2-0":"5"},"Surcharge":"0"},"STJUDE10":{"Code":"STJUDE10","FlavorCode":"","ImageCode":"STJUDE10","Local":false,"Name":"St. Jude Donation","Price":"10","ProductCode":"F_STJUDE","SizeCode":"","Tags":{"Donation":"STJUDE","ExcludeFromLoyalty":true,"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"10","Price1-0":"10","Price1-3":"10","Price2-3":"10","Price1-4":"10","Price2-2":"10","Price1-1":"10","Price2-1":"10","Price1-2":"10","Price2-0":"10"},"Surcharge":"0"},"STJUDERU":{"Code":"STJUDERU","FlavorCode":"","ImageCode":"STJUDERU","Local":false,"Name":"St. Jude Donation","Price":"0","ProductCode":"F_STJUDE","SizeCode":"","Tags":{"NotEditable":true,"Hidden":true,"Donation":"STJUDE","ExcludeFromLoyalty":true,"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"0","Price1-0":"0","Price1-3":"0","Price2-3":"0","Price1-4":"0","Price2-2":"0","Price1-1":"0","Price2-1":"0","Price1-2":"0","Price2-0":"0"},"Surcharge":"0"},"CEABVI":{"Code":"CEABVI","FlavorCode":"","ImageCode":"CEABVI","Local":false,"Name":"Balsamic","Price":"0.75","ProductCode":"F_BALVIN","SizeCode":"","Tags":{"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"0.75","Price1-0":"0.75","Price1-3":"0.75","Price2-3":"0.75","Price1-4":"0.75","Price2-2":"0.75","Price1-1":"0.75","Price2-1":"0.75","Price1-2":"0.75","Price2-0":"0.75"},"Surcharge":"0"},"_SCHOOLL":{"Code":"_SCHOOLL","FlavorCode":"","ImageCode":"_SCHOOLL","Local":true,"Name":"Local Donation","Price":"0","ProductCode":"F__SCHOOL","SizeCode":"","Tags":{"Donation":"SCHOOL"," ExcludeFromLoyalty":true,"DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":false,"Pricing":{"Price2-4":"0","Price1-0":"0","Price1-3":"0","Price2-3":"0","Price1-4":"0","Price2-2":"0","Price1-1":"0","Price2-1":"0","Price1-2":"0","Price2-0":"0"},"Surcharge":"0"},"W08PBNLW":{"Code":"W08PBNLW","FlavorCode":"BCHICK","ImageCode":"W08PBNLW","Local":false,"Name":"8-Piece Boneless Chicken","Price":"7.99","ProductCode":"S_BONELESS","SizeCode":"8PCW","Tags":{"Boneless":true,"EffectiveOn":"2011-02-21","DefaultSides":"HOTCUP=1","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"7.99","Price1-0":"7.99","Price1-3":"7.99","Price2-3":"7.99","Price1-4":"7.99","Price2-2":"7.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"7.99","Price2-0":"7.99"},"Surcharge":"0"},"W08PHOTW":{"Code":"W08PHOTW","FlavorCode":"HOTWINGS","ImageCode":"W08PHOTW","Local":false,"Name":"8-piece Hot Wings","Price":"7.99","ProductCode":"S_HOTWINGS","SizeCode":"8PCW","Tags":{"BundleBuilderProducts":true,"Wings":true,"EffectiveOn":"2011-02-21","sodiumWarningEnabled":true,"DefaultSides":"Bd=1","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"7.99","Price1-0":"7.99","Price1-3":"7.99","Price2-3":"7.99","Price1-4":"7.99","Price2-2":"7.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"7.99","Price2-0":"7.99"},"Surcharge":"0"},"W08PBBQW":{"Code":"W08PBBQW","FlavorCode":"BBQW","ImageCode":"W08PBBQW","Local":false,"Name":"8-Piece BBQ Wings","Price":"7.99","ProductCode":"S_BBQW","SizeCode":"8PCW","Tags":{"BundleBuilderProducts":true,"Wings":true,"EffectiveOn":"2011-02-21","DefaultSides":"Bd=1","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"7.99","Price1-0":"7.99","Price1-3":"7.99","Price2-3":"7.99","Price1-4":"7.99","Price2-2":"7.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"7.99","Price2-0":"7.99"},"Surcharge":"0"},"W08PPLNW":{"Code":"W08PPLNW","FlavorCode":"PLNWINGS","ImageCode":"W08PPLNW","Local":false,"Name":"8-piece Plain Wings","Price":"7.99","ProductCode":"S_PLNWINGS","SizeCode":"8PCW","Tags":{"BundleBuilderProducts":true,"Wings":true,"EffectiveOn":"2011-02-21","DefaultSides":"Bd=1","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"7.99","Price1-0":"7.99","Price1-3":"7.99","Price2-3":"7.99","Price1-4":"7.99","Price2-2":"7.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"7.99","Price2-0":"7.99"},"Surcharge":"0"},"W08PMANW":{"Code":"W08PMANW","FlavorCode":"SMANG","ImageCode":"W08PMANW","Local":false,"Name":"8-Piece Sweet Mango Habanero Wings","Price":"7.99","ProductCode":"S_SMANG","SizeCode":"8PCW","Tags":{"BundleBuilderProducts":true,"Wings":true,"EffectiveOn":"2011-02-21","DefaultSides":"Bd=1","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"7.99","Price1-0":"7.99","Price1-3":"7.99","Price2-3":"7.99","Price1-4":"7.99","Price2-2":"7.99","Price1-1":"7.99","Price2-1":"7.99","Price1-2":"7.99","Price2-0":"7.99"},"Surcharge":"0"},"W14PBNLW":{"Code":"W14PBNLW","FlavorCode":"BCHICK","ImageCode":"W14PBNLW","Local":false,"Name":"14-Piece Boneless Chicken","Price":"12.99","ProductCode":"S_BONELESS","SizeCode":"14PCW","Tags":{"Boneless":true,"EffectiveOn":"2011-02-21","DefaultSides":"HOTCUP=2","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"12.99","Price1-0":"12.99","Price1-3":"12.99","Price2-3":"12.99","Price1-4":"12.99","Price2-2":"12.99","Price1-1":"12.99","Price2-1":"12.99","Price1-2":"12.99","Price2-0":"12.99"},"Surcharge":"0"},"W14PHOTW":{"Code":"W14PHOTW","FlavorCode":"HOTWINGS","ImageCode":"W14PHOTW","Local":false,"Name":"14-piece Hot Wings","Price":"12.99","ProductCode":"S_HOTWINGS","SizeCode":"14PCW","Tags":{"BundleBuilderProducts":true,"Wings":true,"EffectiveOn":"2011-02-21","sodiumWarningEnabled":true,"DefaultSides":"Bd=2","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"12.99","Price1-0":"12.99","Price1-3":"12.99","Price2-3":"12.99","Price1-4":"12.99","Price2-2":"12.99","Price1-1":"12.99","Price2-1":"12.99","Price1-2":"12.99","Price2-0":"12.99"},"Surcharge":"0"},"W14PBBQW":{"Code":"W14PBBQW","FlavorCode":"BBQW","ImageCode":"W14PBBQW","Local":false,"Name":"14-Piece BBQ Wings","Price":"12.99","ProductCode":"S_BBQW","SizeCode":"14PCW","Tags":{"BundleBuilderProducts":true,"Wings":true,"sodiumWarningEnabled":true,"EffectiveOn":"2011-02-21","DefaultSides":"Bd=2","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"12.99","Price1-0":"12.99","Price1-3":"12.99","Price2-3":"12.99","Price1-4":"12.99","Price2-2":"12.99","Price1-1":"12.99","Price2-1":"12.99","Price1-2":"12.99","Price2-0":"12.99"},"Surcharge":"0"},"W14PPLNW":{"Code":"W14PPLNW","FlavorCode":"PLNWINGS","ImageCode":"W14PPLNW","Local":false,"Name":"14-piece Plain Wings","Price":"12.99","ProductCode":"S_PLNWINGS","SizeCode":"14PCW","Tags":{"BundleBuilderProducts":true,"Wings":true,"EffectiveOn":"2011-02-21","DefaultSides":"Bd=2","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"12.99","Price1-0":"12.99","Price1-3":"12.99","Price2-3":"12.99","Price1-4":"12.99","Price2-2":"12.99","Price1-1":"12.99","Price2-1":"12.99","Price1-2":"12.99","Price2-0":"12.99"},"Surcharge":"0"},"W14PMANW":{"Code":"W14PMANW","FlavorCode":"SMANG","ImageCode":"W14PMANW","Local":false,"Name":"14-Piece Sweet Mango Habanero Wings","Price":"12.99","ProductCode":"S_SMANG","SizeCode":"14PCW","Tags":{"BundleBuilderProducts":true,"Wings":true,"EffectiveOn":"2011-02-21","DefaultSides":"Bd=2","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"12.99","Price1-0":"12.99","Price1-3":"12.99","Price2-3":"12.99","Price1-4":"12.99","Price2-2":"12.99","Price1-1":"12.99","Price2-1":"12.99","Price1-2":"12.99","Price2-0":"12.99"},"Surcharge":"0"},"W40PBNLW":{"Code":"W40PBNLW","FlavorCode":"BCHICK","ImageCode":"W40PBNLW","Local":false,"Name":"40-Piece Boneless Chicken","Price":"31.99","ProductCode":"S_BONELESS","SizeCode":"40PCW","Tags":{"Boneless":true,"EffectiveOn":"2011-02-21","DefaultSides":"HOTCUP=5","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"31.99","Price1-0":"31.99","Price1-3":"31.99","Price2-3":"31.99","Price1-4":"31.99","Price2-2":"31.99","Price1-1":"31.99","Price2-1":"31.99","Price1-2":"31.99","Price2-0":"31.99"},"Surcharge":"0"},"W40PHOTW":{"Code":"W40PHOTW","FlavorCode":"HOTWINGS","ImageCode":"W40PHOTW","Local":false,"Name":"40-piece Hot Wings","Price":"31.99","ProductCode":"S_HOTWINGS","SizeCode":"40PCW","Tags":{"BundleBuilderProducts":true,"Wings":true,"EffectiveOn":"2011-02-21","sodiumWarningEnabled":true,"DefaultSides":"Bd=5","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"31.99","Price1-0":"31.99","Price1-3":"31.99","Price2-3":"31.99","Price1-4":"31.99","Price2-2":"31.99","Price1-1":"31.99","Price2-1":"31.99","Price1-2":"31.99","Price2-0":"31.99"},"Surcharge":"0"},"W40PBBQW":{"Code":"W40PBBQW","FlavorCode":"BBQW","ImageCode":"W40PBBQW","Local":false,"Name":"40-Piece BBQ Wings","Price":"31.99","ProductCode":"S_BBQW","SizeCode":"40PCW","Tags":{"BundleBuilderProducts":true,"Wings":true,"EffectiveOn":"2011-02-21","DefaultSides":"Bd=5","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"31.99","Price1-0":"31.99","Price1-3":"31.99","Price2-3":"31.99","Price1-4":"31.99","Price2-2":"31.99","Price1-1":"31.99","Price2-1":"31.99","Price1-2":"31.99","Price2-0":"31.99"},"Surcharge":"0"},"W40PPLNW":{"Code":"W40PPLNW","FlavorCode":"PLNWINGS","ImageCode":"W40PPLNW","Local":false,"Name":"40-piece Plain Wings","Price":"31.99","ProductCode":"S_PLNWINGS","SizeCode":"40PCW","Tags":{"BundleBuilderProducts":true,"Wings":true,"EffectiveOn":"2011-02-21","DefaultSides":"Bd=5","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"31.99","Price1-0":"31.99","Price1-3":"31.99","Price2-3":"31.99","Price1-4":"31.99","Price2-2":"31.99","Price1-1":"31.99","Price2-1":"31.99","Price1-2":"31.99","Price2-0":"31.99"},"Surcharge":"0"},"W40PMANW":{"Code":"W40PMANW","FlavorCode":"SMANG","ImageCode":"W40PMANW","Local":false,"Name":"40-Piece Sweet Mango Habanero Wings","Price":"31.99","ProductCode":"S_SMANG","SizeCode":"40PCW","Tags":{"BundleBuilderProducts":true,"Wings":true,"EffectiveOn":"2011-02-21","DefaultSides":"Bd=5","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"31.99","Price1-0":"31.99","Price1-3":"31.99","Price2-3":"31.99","Price1-4":"31.99","Price2-2":"31.99","Price1-1":"31.99","Price2-1":"31.99","Price1-2":"31.99","Price2-0":"31.99"},"Surcharge":"0"},"CKRGCBT":{"Code":"CKRGCBT","FlavorCode":"BACTOM","ImageCode":"CKRGCBT","Local":false,"Name":"Specialty Chicken – Crispy Bacon & Tomato","Price":"7.99","ProductCode":"S_SCCBT","SizeCode":"12PCB","Tags":{"SpecialtyChicken":true,"Promotion":"SpChkProductNotInCart","PromotionType":"ProductNotInCart","DefaultSides":"","DefaultToppings":"K=1,Td=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"11.99","Price1-0":"7.99","Price1-3":"10.99","Price2-3":"10.99","Price1-4":"11.99","Price2-2":"9.99","Price1-1":"8.99","Price2-1":"8.99","Price1-2":"9.99","Price2-0":"7.99"},"Surcharge":"0"},"CKRGHTB":{"Code":"CKRGHTB","FlavorCode":"HOTBUFF","ImageCode":"CKRGHTB","Local":false,"Name":"Specialty Chicken – Classic Hot Buffalo","Price":"7.99","ProductCode":"S_SCCHB","SizeCode":"12PCB","Tags":{"SpecialtyChicken":true,"Promotion":"SpChkProductNotInCart","PromotionType":"ProductNotInCart","DefaultSides":"","DefaultToppings":""},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"11.99","Price1-0":"7.99","Price1-3":"10.99","Price2-3":"10.99","Price1-4":"11.99","Price2-2":"9.99","Price1-1":"8.99","Price2-1":"8.99","Price1-2":"9.99","Price2-0":"7.99"},"Surcharge":"0"},"CKRGSJP":{"Code":"CKRGSJP","FlavorCode":"SPCYJP","ImageCode":"CKRGSJP","Local":false,"Name":"Specialty Chicken – Spicy Jalapeno - Pineapple","Price":"7.99","ProductCode":"S_SCSJP","SizeCode":"12PCB","Tags":{"SpecialtyChicken":true,"Promotion":"SpChkProductNotInCart","PromotionType":"ProductNotInCart","DefaultSides":"","DefaultToppings":"J=1,N=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"11.99","Price1-0":"7.99","Price1-3":"10.99","Price2-3":"10.99","Price1-4":"11.99","Price2-2":"9.99","Price1-1":"8.99","Price2-1":"8.99","Price1-2":"9.99","Price2-0":"7.99"},"Surcharge":"0"},"CKRGSBQ":{"Code":"CKRGSBQ","FlavorCode":"BBQBAC","ImageCode":"CKRGSBQ","Local":false,"Name":"Specialty Chicken – Sweet BBQ Bacon","Price":"7.99","ProductCode":"S_SCSBBQ","SizeCode":"12PCB","Tags":{"SpecialtyChicken":true,"Promotion":"SpChkProductNotInCart","PromotionType":"ProductNotInCart","DefaultSides":"","DefaultToppings":"K=1"},"AllowedCookingInstructions":"","DefaultCookingInstructions":"","Prepared":true,"Pricing":{"Price2-4":"11.99","Price1-0":"7.99","Price1-3":"10.99","Price2-3":"10.99","Price1-4":"11.99","Price2-2":"9.99","Price1-1":"8.99","Price2-1":"8.99","Price1-2":"9.99","Price2-0":"7.99"},"Surcharge":"0"}},"PreconfiguredProducts":{"14SCREEN":{"Code":"14SCREEN","Description":"Cheese made with 100% real mozzarella on top of our garlic-seasoned crust with a rich, buttery taste.","Name":"Hand Tossed Large Cheese","Size":"Large (14\")","Options":"","ReferencedProductCode":"14SCREEN","Tags":{"Banner":"vegetarian"}},"B32PBIT":{"Code":"B32PBIT","Description":"Oven baked, bite-size breadsticks sprinkled with Parmesan Asiago cheese & seasoned with garlic and more Parmesan. Perfectly delicious for sharing!","Name":"Parm Bread Bites","Size":"32-Piece","Options":"","ReferencedProductCode":"B32PBIT","Tags":{}},"B8PCSCB":{"Code":"B8PCSCB","Description":"Oven baked breadsticks stuffed with cheese and covered in cheese made with 100% Mozzarella and Cheddar. Seasoned with garlic, parsley and Romano cheese.","Name":"Cheese Stuffed Cheesy Bread","Size":"8-Piece","Options":"","ReferencedProductCode":"B8PCSCB","Tags":{}},"P_14SCREEN":{"Code":"P_14SCREEN","Description":"Pepperoni and cheese made with 100% real mozzarella on top of our garlic-seasoned crust with a rich, buttery taste.","Name":"Hand Tossed Pepperoni","Size":"Large (14\")","Options":"P=1","ReferencedProductCode":"14SCREEN","Tags":{}},"W40PBNLW":{"Code":"W40PBNLW","Description":"Lightly breaded with savory herbs, made with 100% whole white breast meat. Comes with 5 dipping cups.","Name":"Plain Boneless Chicken","Size":"40-Piece","Options":"","ReferencedProductCode":"W40PBNLW","Tags":{}},"S_14SCREEN":{"Code":"S_14SCREEN","Description":"Sausage and cheese made with 100% real mozzarella on top of our garlic-seasoned crust with a rich, buttery taste.","Name":"Hand Tossed Sausage","Size":"Large (14\")","Options":"S=1","ReferencedProductCode":"14SCREEN","Tags":{}},"PS_14SCREEN":{"Code":"PS_14SCREEN","Description":"Pepperoni, sausage and cheese made with 100% real mozzarella on top of our garlic-seasoned crust with a rich, buttery taste.","Name":"Hand Tossed Pepperoni & Sausage","Size":"Large (14\")","Options":"P=1,S=1","ReferencedProductCode":"14SCREEN","Tags":{}},"W14PBNLW":{"Code":"W14PBNLW","Description":"Lightly breaded with savory herbs, made with 100% whole white breast meat. Comes with 2 dipping cups.","Name":"Plain Boneless Chicken","Size":"14-Piece","Options":"","ReferencedProductCode":"W14PBNLW","Tags":{}},"W40PHOTW":{"Code":"W40PHOTW","Description":"Marinated and oven-baked and then sauced with Hot Sauce. Comes with 5 dipping cups.","Name":"Hot Wings","Size":"40-Piece","Options":"","ReferencedProductCode":"W40PHOTW","Tags":{}},"PM_14SCREEN":{"Code":"PM_14SCREEN","Description":"Pepperoni, fresh mushrooms, and cheese made with 100% real mozzarella on top of our garlic-seasoned crust with a rich, buttery taste.","Name":"Hand Tossed Pepperoni & Mushroom","Size":"Large (14\")","Options":"P=1,M=1","ReferencedProductCode":"14SCREEN","Tags":{}},"W14PHOTW":{"Code":"W14PHOTW","Description":"Marinated and oven-baked and then sauced with Hot Sauce. Comes with 2 dipping cups.","Name":"Hot Wings","Size":"14-Piece","Options":"","ReferencedProductCode":"W14PHOTW","Tags":{}},"W40PBBQW":{"Code":"W40PBBQW","Description":"Marinated and oven-baked and then sauced with BBQ Sauce. Comes with 5 dipping cups.","Name":"BBQ Wings","Size":"40-Piece","Options":"","ReferencedProductCode":"W40PBBQW","Tags":{}},"P12IPAZA":{"Code":"P12IPAZA","Description":"Two layers of cheese and a crust that bakes up golden and crispy with a buttery taste.","Name":"Medium Cheese Pan","Size":"Medium (12\")","Options":"","ReferencedProductCode":"P12IPAZA","Tags":{"Banner":"vegetarian"}},"P_P12IPAZA":{"Code":"P_P12IPAZA","Description":"Two layers of cheese, Pepperoni to the edge, and a crust that bakes up golden and crispy with a buttery taste.","Name":"Medium Pepperoni Pan","Size":"Medium (12\")","Options":"P=1","ReferencedProductCode":"P12IPAZA","Tags":{}},"W14PBBQW":{"Code":"W14PBBQW","Description":"Marinated and oven-baked and then sauced with BBQ Sauce. Comes with 2 dipping cups.","Name":"BBQ Wings","Size":"14-Piece","Options":"","ReferencedProductCode":"W14PBBQW","Tags":{}},"P_P10IGFZA":{"Code":"P_P10IGFZA","Description":"Domino's pepperoni pizza made on a Gluten Free Crust.","Name":"Small Gluten Free Crust Pepperoni","Size":"Small (10\")","Options":"P=1","ReferencedProductCode":"P10IGFZA","Tags":{"Banner":"glutenFree"}},"MARBRWNE":{"Code":"MARBRWNE","Description":"Satisfy your sweet tooth! Taste the decadent blend of gooey milk chocolate chunk cookie and delicious fudge brownie. Oven-baked to perfection and cut into 9 pieces - this dessert is perfect to share with the whole group","Name":"Domino's Marbled Cookie Brownie™ ","Size":"9-Piece","Options":"","ReferencedProductCode":"MARBRWNE","Tags":{}},"14SCEXTRAV":{"Code":"14SCEXTRAV","Description":"Loads of pepperoni, ham, Italian sausage, beef, onions, green peppers, mushrooms and black olives topped with extra cheese made with 100% real mozzarella.","Name":"Hand Tossed ExtravaganZZa","Size":"Large (14\")","Options":"","ReferencedProductCode":"14SCEXTRAV","Tags":{}},"P14ITHPV":{"Code":"P14ITHPV","Description":"Roasted red peppers, baby spinach, onions, mushrooms, tomatoes, black olives, cheeses made with 100% real mozzarella, feta and provolone on a crispy thin crust.","Name":"Thin Crust Pacific Veggie Pizza","Size":"Large (14\")","Options":"","ReferencedProductCode":"P14ITHPV","Tags":{"Banner":"vegetarian"}},"2LCOKE":{"Code":"2LCOKE","Description":"The authentic cola sensation that is a refreshing part of sharing life's enjoyable moments","Name":"Coke","Size":"2-Liter Bottle","Options":"","ReferencedProductCode":"2LCOKE","Tags":{}},"2LDCOKE":{"Code":"2LDCOKE","Description":"Beautifully balanced adult cola taste in a no calorie beverage","Name":"Diet Coke","Size":"2-Liter Bottle","Options":"","ReferencedProductCode":"2LDCOKE","Tags":{}},"2LSPRITE":{"Code":"2LSPRITE","Description":"Unique Lymon (lemon-lime) flavor, clear, clean and crisp with no caffeine.","Name":"Sprite","Size":"2-Liter Bottle","Options":"","ReferencedProductCode":"2LSPRITE","Tags":{}},"XC_14SCREEN":{"Code":"XC_14SCREEN","Description":"","Name":"Large (14\") Hand Tossed Pizza","Size":"Large (14\")","Options":"X=1,C=1","ReferencedProductCode":"14SCREEN","Tags":{}},"PXC_14SCREEN":{"Code":"PXC_14SCREEN","Description":"","Name":"Large (14\") Hand Tossed Pizza Whole: Pepperoni","Size":"Large (14\")","Options":"P=1,X=1,C=1","ReferencedProductCode":"14SCREEN","Tags":{}},"MPXC_12SCREEN":{"Code":"MPXC_12SCREEN","Description":"","Name":"Medium (12\") Hand Tossed Pizza Whole : Mushrooms, Pepperoni","Size":"Medium (12\")","Options":"M=1,P=1,X=1,C=1","ReferencedProductCode":"12SCREEN","Tags":{}},"XCFeCsCpRMORrSiTd_P12IREPV":{"Code":"XCFeCsCpRMORrSiTd_P12IREPV","Description":"","Name":"Medium (12\") Hand Tossed Pacific Veggie Pizza","Size":"Medium (12\")","Options":"X=1,C=1,Fe=1,Cs=1,Cp=1,R=1,M=1,O=1,Rr=1,Si=1,Td=1","ReferencedProductCode":"P12IREPV","Tags":{}},"RdCKDuPv_PSANSACB":{"Code":"RdCKDuPv_PSANSACB","Description":"","Name":"Chicken Bacon Ranch Sandwich","Size":"Sandwich","Options":"Rd=1,C=1,K=1,Du=1,Pv=1","ReferencedProductCode":"PSANSACB","Tags":{}},"XfDu_PINPASCA":{"Code":"XfDu_PINPASCA","Description":"","Name":"Chicken Alfredo Pasta","Size":"Individual","Options":"Xf=1,Du=1","ReferencedProductCode":"PINPASCA","Tags":{}},"SIDRAN_W08PBBQW":{"Code":"SIDRAN_W08PBBQW","Description":"","Name":"8-Piece BBQ Wings (1) Ranch","Size":"8-Piece","Options":"SIDRAN=1","ReferencedProductCode":"W08PBBQW","Tags":{}},"B2PCLAVA":{"Code":"B2PCLAVA","Description":"","Name":"2-Piece Chocolate Lava Crunch Cakes","Size":"2-Piece","Options":"","ReferencedProductCode":"B2PCLAVA","Tags":{}}},"ShortProductDescriptions":{"B8PCPT":{"Code":"B8PCPT","Description":"Drizzled with garlic and Parmesan cheese seasoning and sprinkled with more Parmesan cheese. Served with marinara sauce."},"B8PCGT":{"Code":"B8PCGT","Description":"Drizzled with buttery garlic and Parmesan cheese seasoning. Served with marinara sauce."},"B8PCCT":{"Code":"B8PCCT","Description":"Drizzled with delicious cinnamon and sugar to satisfy any sweet tooth. Served with sweet icing."},"B16PBIT":{"Code":"B16PBIT","Description":"Handmade, oven-baked bread bites seasoned with garlic and Parmesan."},"B2PCLAVA":{"Code":"B2PCLAVA","Description":"Indulge in two delectable oven-baked chocolate cakes with molten chocolate fudge on the inside. Perfectly topped with a dash of powdered sugar."},"MARBRWNE":{"Code":"MARBRWNE","Description":"Taste the decadent blend of gooey milk chocolate chunk cookie and delicious fudge brownie. Oven baked with 9 pieces to make it perfectly shareable."},"2LCOKE":{"Code":"2LCOKE","Description":"The authentic cola sensation that is a refreshing part of sharing life's enjoyable moments."},"2LDCOKE":{"Code":"2LDCOKE","Description":"Beautifully balanced adult cola taste in a no calorie beverage."},"CKRGSBQ":{"Code":"CKRGSBQ","Description":"Tender bites of 100% whole breast white meat chicken, topped with sweet and smoky BBQ sauce, mozzarella and cheddar cheese, and crispy bacon."},"CKRGHTB":{"Code":"CKRGHTB","Description":"Tender bites of 100% whole breast white meat chicken, topped with classic hot buffalo sauce, ranch, mozzarella and cheddar cheese, and feta."},"PSANSAPH":{"Code":"PSANSAPH","Description":"Experience delicious slices of steak topped with American and provolone cheese, fresh onions, green peppers and mushrooms. Oven-baked to perfection."},"PSANSACB":{"Code":"PSANSACB","Description":"Enjoy our flavorful grilled chicken breast topped with smoked bacon, ranch and provolone cheese on artisan bread baked to golden brown perfection."},"PINPASCA":{"Code":"PINPASCA","Description":"Try our savory Chicken Alfredo Pasta. Grilled chicken breast and creamy Alfredo sauce is mixed with penne pasta and baked to creamy perfection."},"PINPASCC":{"Code":"PINPASCC","Description":"Taste the delectable blend of grilled chicken, smoked bacon, onions and mushrooms mixed with penne pasta. Topped with rich Alfredo sauce."},"PPSGARSA":{"Code":"PPSGARSA","Description":"A crisp combination of grape tomatoes, red onion, carrots, red cabbage, cheddar cheese and croutons on a blend of romaine and iceberg lettuce."},"PPSCSRSA":{"Code":"PPSCSRSA","Description":"The makings of a classic: roasted white meat chicken, Parmesan cheese and croutons, all atop a blend of romaine and iceberg lettuce."},"PPSCAPSA":{"Code":"PPSCAPSA","Description":"Roasted white meat chicken, diced red and green apples, dried cranberries, praline pecans and cheddar cheese paired with a leafy spring mix."}},"CouponTiers":{"MultiplePizzaC":{"Code":"MultiplePizzaC","Coupons":{"8651C":{"Code":"8651C","CouponTierThreshold":7,"CouponTierPercentOff":15,"Name":"15% off all pizzas","Description":"15% de descuento todas las pizzas ","ServiceMethod":""},"8652C":{"Code":"8652C","CouponTierThreshold":10,"CouponTierPercentOff":20,"Name":"20% off all pizzas","Description":"20% de descuento todas las pizzas","ServiceMethod":""}}},"MultiplePizza":{"Code":"MultiplePizza","Coupons":{"8650":{"Code":"8650","CouponTierThreshold":4,"CouponTierPercentOff":10,"Name":"10% off all pizzas","Description":"Group order Discount: 10% off any pizza at menu price. Online only when you order 4+ pizzas","ServiceMethod":""},"8651":{"Code":"8651","CouponTierThreshold":7,"CouponTierPercentOff":15,"Name":"15% off all pizzas","Description":"Group order Discount: 15% off any pizza at menu price. Online only when you order 7+ pizzas","ServiceMethod":""},"8652":{"Code":"8652","CouponTierThreshold":10,"CouponTierPercentOff":20,"Name":"20% off all pizzas","Description":"Group Order Discount: 20% off any pizza at menu price. Online Only when you order 10+ pizzas","ServiceMethod":""}}}},"UnsupportedProducts":{"DN2":{"PulseCode":"DN2","Description":"Each DN2"},"_PLATE":{"PulseCode":"_PLATE","Description":"Each Plate"},"S14BWGCS":{"PulseCode":"S14BWGCS","Description":"14\" B Whole Grain SMT Cheese"},"DN4":{"PulseCode":"DN4","Description":"Each DN4"},"PINPASBA":{"PulseCode":"PINPASBA","Description":"Build Your Own Pasta"},"PINBBLBA":{"PulseCode":"PINBBLBA","Description":"Build your Own BreadBowl Pasta"},"S14BWGPP":{"PulseCode":"S14BWGPP","Description":"14\" B Whole Grain SMT Pepperoni"},"PINPASBM":{"PulseCode":"PINPASBM","Description":"Build Your Own Pasta"},"DN3":{"PulseCode":"DN3","Description":"Each DN3"},"PINBBLBM":{"PulseCode":"PINBBLBM","Description":"Build your Own BreadBowl Pasta"}},"UnsupportedOptions":{"Bd":{"PulseCode":"Bd","Description":"BleuCheese"},"Bq":{"PulseCode":"Bq","Description":"BBQ Sauce"},"Cp":{"PulseCode":"Cp","Description":"Shredded Provolone"},"Cs":{"PulseCode":"Cs","Description":"Shredded Parm/Asiago"},"Du":{"PulseCode":"Du","Description":"Premium Chicken"},"E":{"PulseCode":"E","Description":"Cheddar Ches"},"Fe":{"PulseCode":"Fe","Description":"Feta Cheese"},"G":{"PulseCode":"G","Description":"Green Pepper"},"H":{"PulseCode":"H","Description":"Ham"},"Ht":{"PulseCode":"Ht","Description":"Buffalo Sauce"},"J":{"PulseCode":"J","Description":"Jalapeno Pep"},"K":{"PulseCode":"K","Description":"Bacon"},"M":{"PulseCode":"M","Description":"Mushrooms"},"Mc":{"PulseCode":"Mc","Description":"Cheese Lite Mozz"},"N":{"PulseCode":"N","Description":"Pineapple"},"O":{"PulseCode":"O","Description":"Onions"},"P":{"PulseCode":"P","Description":"Pepperoni"},"R":{"PulseCode":"R","Description":"Black Olives"},"Rr":{"PulseCode":"Rr","Description":"Roasted Red Peppers"},"S":{"PulseCode":"S","Description":"Sausage"},"Si":{"PulseCode":"Si","Description":"Fresh Spinach"},"Td":{"PulseCode":"Td","Description":"Diced Tomatoes"},"X":{"PulseCode":"X","Description":"Original Sauce"}},"CookingInstructions":{"WD":{"Code":"WD","Name":"Well Done","Description":"","Group":"BAKE"},"NB":{"Code":"NB","Name":"Normal Bake","Description":"","Group":"BAKE"},"GO":{"Code":"GO","Name":"Garlic-Seasoned Crust","Description":"","Group":"SEASONING"},"NGO":{"Code":"NGO","Name":"No Garlic-Seasoned Crust","Description":"","Group":"SEASONING"},"RGO":{"Code":"RGO","Name":"Oregano","Description":"","Group":"SEASONING"},"PIECT":{"Code":"PIECT","Name":"Pie Cut","Description":"","Group":"CUT"},"SQCT":{"Code":"SQCT","Name":"Square Cut","Description":"","Group":"CUT"},"UNCT":{"Code":"UNCT","Name":"Uncut","Description":"","Group":"CUT"},"NOOR":{"Code":"NOOR","Name":"No Oregano","Description":"","Group":"SEASONING"}},"CookingInstructionGroups":{"BAKE":{"Code":"BAKE","Name":"Bake","Tags":{}},"SEASONING":{"Code":"SEASONING","Name":"Seasoning","Tags":{}},"CUT":{"Code":"CUT","Name":"Cut","Tags":{"MaxOptions":"1"}}}} \ No newline at end of file diff --git a/dawg/testdata/order-meta.json b/dawg/testdata/order-meta.json new file mode 100644 index 0000000..1c94b04 --- /dev/null +++ b/dawg/testdata/order-meta.json @@ -0,0 +1 @@ +{"customerOrders":[{"addressNickName":"home","cards":[{"id":"","nickName":""}],"deliveryInstructions":"","id":"","order":{"Partners":{},"Address":{"City":"","Name":"home","PostalCode":"","Region":"","Street":"","StreetName":"","StreetNumber":"","Type":"House"},"Amounts":{"Adjustment":0,"Bottle":0,"Customer":41.62,"Discount":0,"Menu":37.92,"Net":37.92,"Payment":41.62,"Surcharge":3.99,"Tax":3.7,"Tax1":3.7,"Tax2":0},"AmountsBreakdown":{"Adjustment":"0.00","Bottle":0,"Customer":41.62,"DeliveryFee":"3.99","FoodAndBeverage":"33.93","Savings":"0.00","Surcharge":"0.00","Tax":3.7,"Tax1":3.7,"Tax2":0},"AvailablePromos":{},"BusinessDate":"2020-01-02","Coupons":[],"Currency":"USD","CustomerID":"","Email":"","EstimatedWaitMinutes":"41-51","Extension":"","FirstName":"","IP":"0","LanguageCode":"en","LastName":"","Market":"","NoCombine":true,"OrderChannel":"OLO","OrderID":"","OrderInfoCollection":[],"OrderMethod":"Web","Payments":[{"Amount":41.62111111111,"CardID":"","CardType":"","Expiration":"0","Number":"0","PostalCode":"0","SecurityCode":"XXX","StatusItems":[],"Type":"CreditCard"}],"Phone":"0","PhonePrefix":"","PlaceOrderMs":4288,"PlaceOrderTime":"2020-02-02 22:00:00","Products":[{"Options":{"P":{"1/1":"1.5"},"C":{"1/1":"2"}},"AutoRemove":false,"CategoryCode":"Pizza","Code":"16SCREEN","CouponIDs":[],"descriptions":[{"portionCode":"1/1","value":"Extra Pepperoni, Double Cheese, Robust Inspired Tomato Sauce"}],"FlavorCode":"HANDTOSS","Fulfilled":false,"ID":1,"LikeProductID":0,"name":"X-Large (16\") Hand Tossed Pizza","NeedsCustomization":false,"Price":21.94,"Qty":1,"Status":0,"StatusItems":[],"Tags":{}},{"Options":{},"AutoRemove":false,"CategoryCode":"Wings","Code":"W14PBBQW","CouponIDs":[],"descriptions":[{"portionCode":"1/1","value":"Double Ranch"}],"FlavorCode":"BBQW","Fulfilled":false,"ID":2,"LikeProductID":0,"name":"14-Piece BBQ Wings","NeedsCustomization":false,"Price":11.99,"Qty":1,"Status":0,"StatusItems":[],"Tags":{}}],"Promotions":{"Redeemable":[],"Valid":[]},"RemovedProducts":false,"ServiceMethod":"Delivery","SourceOrganizationURI":"order.dominos.com","Status":0,"StatusItems":[{"Code":"PrsComplete","RemovedCoupons":[],"RemovedProducts":[]}],"StoreID":"1","StoreOrderID":"2020-06-04#1111","StorePlaceOrderTime":"2020-06-04 19:06:12","Tags":{},"Version":"1.0","OrderMessages":[]},"store":{"address":{"City":"","PostalCode":"","Region":"","Street":""},"carryoutServiceHours":"Su-Sa 10:30am-10:00pm","deliveryServiceHours":"Su-Th 10:30am-12:00am\nFr-Sa 10:30am-1:00am"}},{"addressNickName":"default","cards":[{"id":"","nickName":"TheDebit"}],"deliveryInstructions":"","id":"","order":{"Partners":{},"Address":{"City":"","IsDefault":true,"Name":"default","PostalCode":"","Region":"","Street":"","StreetName":"","StreetNumber":"","Type":"House"},"Amounts":{"Adjustment":16.49,"Bottle":0,"Customer":4.32,"Discount":0,"Menu":20.48,"Net":3.99,"Payment":4.32,"Surcharge":3.99,"Tax":0.33,"Tax1":0.33,"Tax2":0},"AmountsBreakdown":{"Adjustment":"16.49","Bottle":0,"Customer":4.32,"DeliveryFee":"3.99","FoodAndBeverage":"0.00","Savings":"16.49","Surcharge":"0.00","Tax":0.33,"Tax1":0.33,"Tax2":0},"AvailablePromos":{},"BusinessDate":"2020-01-01","Coupons":[],"Currency":"USD","CustomerID":"","Email":"","EstimatedWaitMinutes":"49-59","Extension":"","FirstName":"","IP":"","LanguageCode":"en","LastName":"","Market":"UNITED_STATES","NoCombine":true,"OrderChannel":"OLO","OrderID":"","OrderInfoCollection":[],"OrderMethod":"Web","Payments":[{"Amount":4.32,"CardID":"","CardType":"","Expiration":"0","Number":"0","PostalCode":"0","StatusItems":[],"Type":"CreditCard"}],"Phone":"1111111111","PhonePrefix":"","PlaceOrderMs":4239,"PlaceOrderTime":"2020-01-01 00:00:00","Products":[{"Options":{"P":{"1/1":"2"}},"CategoryCode":"Pizza","Code":"12SCREEN","CouponIDs":[],"descriptions":[{"portionCode":"1/1","value":"Double Pepperoni, Robust Inspired Tomato Sauce, Cheese"}],"FlavorCode":"HANDTOSS","Fulfilled":false,"ID":1,"LikeProductID":0,"name":"Medium (12\") Hand Tossed Pizza","NeedsCustomization":false,"Price":0,"Qty":1,"SizeCode":"12","SpecialtyCode":"PIZZA","Status":0,"StatusItems":[],"Tags":{}}],"Promotions":{"Redeemable":[],"Valid":[]},"RemovedProducts":false,"ServiceMethod":"Delivery","SourceOrganizationURI":"order.dominos.com","Status":0,"StatusItems":[],"StoreID":"1111","StoreOrderID":"2020-01-01#0000","StorePlaceOrderTime":"2020-01-01 00:00:00","Tags":{},"Version":"1.0","OrderMessages":[]},"store":{"address":{"City":"","PostalCode":"","Region":"","Street":""},"carryoutServiceHours":"Su-Sa 10:00am-10:00pm","deliveryServiceHours":"Su-Th 10:00am-12:00am\nFr-Sa 10:00am-1:00am","storeName":""}},{"cards":[{"id":"b6fc7147-2696-4c14-ba1a-8604b15ea394","nickName":"My Debit"}],"id":"","order":{"Partners":{},"Address":{"City":"","DeliveryInstructions":"do the thing","OrganizationName":"","PostalCode":"","Region":"","Street":"","StreetName":"","StreetNumber":"","Type":"Apartment","UnitNumber":"000","UnitType":"#"},"Amounts":{"Adjustment":0,"Bottle":0,"Customer":26.2,"Discount":0,"Menu":23.98,"Net":23.98,"Payment":26.2,"Surcharge":3.99,"Tax":2.22,"Tax1":2.22,"Tax2":0},"AmountsBreakdown":{"Adjustment":"0.00","Bottle":0,"Customer":26.2,"DeliveryFee":"3.99","FoodAndBeverage":"19.99","Savings":"0.00","Surcharge":"0.00","Tax":2,"Tax1":2,"Tax2":0},"AvailablePromos":{},"BusinessDate":"2020-03-25","Coupons":[],"Currency":"USD","CustomerID":"","Email":"","EstimatedWaitMinutes":"35-50","Extension":"","FirstName":"","IP":"00.000.000.000","LanguageCode":"en","LastName":"","Market":"UNITED_STATES","NoCombine":true,"OrderChannel":"OLO","OrderID":"","OrderInfoCollection":[],"OrderMethod":"Web","Payments":[{"Amount":26.2,"CardID":"","CardType":"yeets","Expiration":"0101","Number":"0000","PostalCode":"00000","SecurityCode":"XXX","StatusItems":[],"Type":"CreditCard"}],"Phone":"1111111111","PhonePrefix":"","PlaceOrderMs":3564,"PlaceOrderTime":"2020-03-25 00:00:00","Products":[{"Options":{"P":{"1/1":"1.5"}},"AutoRemove":false,"CategoryCode":"Pizza","Code":"14SCREEN","CouponIDs":[],"descriptions":[{"portionCode":"1/1","value":"Extra Pepperoni, Robust Inspired Tomato Sauce, Cheese"}],"FlavorCode":"HANDTOSS","Fulfilled":false,"ID":1,"LikeProductID":0,"name":"Large (14\") Hand Tossed Pizza","NeedsCustomization":false,"Price":19.99,"Qty":1,"Status":0,"StatusItems":[],"Tags":{}}],"Promotions":{"Redeemable":[],"Valid":[]},"RemovedProducts":false,"ServiceMethod":"Delivery","SourceOrganizationURI":"order.dominos.com","Status":0,"StatusItems":[],"StoreID":"0000","StoreOrderID":"2020-03-25#000","StorePlaceOrderTime":"2020-03-25 00:00:00","Tags":{},"Version":"1.0","OrderMessages":[]},"store":{"address":{"City":"","PostalCode":"","Region":"","Street":""},"carryoutServiceHours":"Su-Sa 10:30am-10:00pm","deliveryServiceHours":"Su-Th 10:30am-12:00am\nFr-Sa 10:30am-1:00am"}}],"easyOrder":{"addressNickName":"home","cards":[],"deliveryInstructions":"","easyOrder":true,"easyOrderNickName":"yeet","id":"","order":{"Partners":{},"Address":{"City":"","IsDefault":true,"Name":"home","PostalCode":"","Region":"","Street":"","StreetName":"","StreetNumber":"","Type":"House"},"Amounts":{"Adjustment":0,"Bottle":0,"Customer":30.57,"Discount":0,"Menu":27.85,"Net":27.85,"Payment":30.57,"Surcharge":3.99,"Tax":2.72,"Tax1":2.72,"Tax2":0},"AmountsBreakdown":{"Adjustment":"0.00","Bottle":0,"Customer":30.57,"DeliveryFee":"3.99","FoodAndBeverage":"23.00","Savings":"0.00","Surcharge":"0.00","Tax":2.72,"Tax1":2.72,"Tax2":0},"AvailablePromos":{},"BusinessDate":"2019-03-28","Coupons":[],"Currency":"USD","CustomerID":"","Email":"","EstimatedWaitMinutes":"24-34","Extension":"","FirstName":"","IP":"00.000.00.00","LanguageCode":"en","LastName":"","Market":"UNITED_STATES","NoCombine":true,"OrderChannel":"OLO","OrderID":"","OrderInfoCollection":[],"OrderMethod":"Web","Payments":[{"Amount":30,"CardType":"","Expiration":"0101","Number":"0000","PostalCode":"00000","SecurityCode":"XXX","StatusItems":[],"Type":"CreditCard"}],"Phone":"1111111111","PhonePrefix":"","PlaceOrderMs":2936,"PlaceOrderTime":"2019-03-29 00:00:00","Products":[{"Options":{"P":{"1/1":"1.5"}},"AutoRemove":false,"CategoryCode":"Pizza","Code":"16SCREEN","CouponIDs":[],"descriptions":[{"portionCode":"1/1","value":"Extra Pepperoni, Robust Inspired Tomato Sauce, Cheese"}],"FlavorCode":"HANDTOSS","Fulfilled":false,"ID":1,"LikeProductID":0,"name":"X-Large (16\") Hand Tossed Pizza","NeedsCustomization":false,"Price":20.29,"Qty":1,"Status":0,"StatusItems":[],"Tags":{}},{"Options":{},"AutoRemove":false,"CategoryCode":"Drinks","Code":"2LCOKE","CouponIDs":[],"descriptions":[],"FlavorCode":"COKE","Fulfilled":false,"ID":2,"LikeProductID":0,"name":"2-Liter Coke®","NeedsCustomization":false,"Price":3.57,"Qty":1,"Status":0,"StatusItems":[],"Tags":{}}],"Promotions":{"Redeemable":[],"Valid":[]},"RemovedProducts":false,"ServiceMethod":"Delivery","SourceOrganizationURI":"order.dominos.com","Status":0,"StatusItems":[],"StoreID":"1234","StoreOrderID":"2019-03-28#111111","StorePlaceOrderTime":"2019-03-28 00:00:00","Tags":{},"Version":"1.0","OrderMessages":[]},"store":{"address":{"City":"","PostalCode":"","Region":"","Street":""},"carryoutServiceHours":"Su-Sa 10:30am-10:00pm","deliveryServiceHours":"Su-Th 10:30am-12:00am\nFr-Sa 10:30am-1:00am"}},"productsByCategory":[{"category":"Pizza","productKeys":["16SCREEN~HANDTOSS~null~C1/1~2P1/1~1.5","12SCREEN~HANDTOSS~null~P1/1~2","14SCREEN~HANDTOSS~null~P1/1~1.5"]},{"category":"Wings","productKeys":["W14PBBQW~BBQW~null~No Options"]}],"productsByFrequencyRecency":[{"productKey":"16SCREEN~HANDTOSS~null~C1/1~2P1/1~1.5","frequency":1},{"productKey":"W14PBBQW~BBQW~null~No Options","frequency":1},{"productKey":"12SCREEN~HANDTOSS~null~P1/1~2","frequency":1},{"productKey":"14SCREEN~HANDTOSS~null~P1/1~1.5","frequency":1}],"products":{"16SCREEN~HANDTOSS~null~C1/1~2P1/1~1.5":{"Options":{"P":{"1/1":"1.5"},"C":{"1/1":"2"}},"AutoRemove":false,"CategoryCode":"Pizza","Code":"16SCREEN","CouponIDs":[],"descriptions":[{"portionCode":"1/1","value":"Extra Pepperoni, Double Cheese, Robust Inspired Tomato Sauce"}],"FlavorCode":"HANDTOSS","Fulfilled":false,"ID":1,"LikeProductID":0,"name":"X-Large (16\") Hand Tossed Pizza","NeedsCustomization":false,"Price":21.94,"Qty":1,"Status":0,"StatusItems":[],"Tags":{}},"W14PBBQW~BBQW~null~No Options":{"Options":{},"AutoRemove":false,"CategoryCode":"Wings","Code":"W14PBBQW","CouponIDs":[],"descriptions":[{"portionCode":"1/1","value":"Double Ranch"}],"FlavorCode":"BBQW","Fulfilled":false,"ID":2,"LikeProductID":0,"name":"14-Piece BBQ Wings","NeedsCustomization":false,"Price":11.99,"Qty":1,"Status":0,"StatusItems":[],"Tags":{}},"14SCREEN~HANDTOSS~null~P1/1~1.5":{"Options":{"P":{"1/1":"1.5"}},"AutoRemove":false,"CategoryCode":"Pizza","Code":"14SCREEN","CouponIDs":[],"descriptions":[{"portionCode":"1/1","value":"Extra Pepperoni, Robust Inspired Tomato Sauce, Cheese"}],"FlavorCode":"HANDTOSS","Fulfilled":false,"ID":1,"LikeProductID":0,"name":"Large (14\") Hand Tossed Pizza","NeedsCustomization":false,"Price":19.99,"Qty":1,"Status":0,"StatusItems":[],"Tags":{}},"12SCREEN~HANDTOSS~null~P1/1~2":{"Options":{"P":{"1/1":"2"}},"CategoryCode":"Pizza","Code":"12SCREEN","CouponIDs":[],"descriptions":[{"portionCode":"1/1","value":"Double Pepperoni, Robust Inspired Tomato Sauce, Cheese"}],"FlavorCode":"HANDTOSS","Fulfilled":false,"ID":1,"LikeProductID":0,"name":"Medium (12\") Hand Tossed Pizza","NeedsCustomization":false,"Price":0,"Qty":1,"SizeCode":"12","SpecialtyCode":"PIZZA","Status":0,"StatusItems":[],"Tags":{}}}} \ No newline at end of file diff --git a/dawg/testdata/store-locator.json b/dawg/testdata/store-locator.json new file mode 100644 index 0000000..e721ef2 --- /dev/null +++ b/dawg/testdata/store-locator.json @@ -0,0 +1 @@ +{"Status":0,"Granularity":"Exact","Address":{"Street":"1600 PENNSYLVANIA AVE NW","StreetNumber":"1600","StreetName":"PENNSYLVANIA AVE NW","UnitType":"","UnitNumber":"","City":"WASHINGTON","Region":"DC","PostalCode":"20500-0003"},"Stores":[{"StoreID":"4336","IsDeliveryStore":true,"MinDistance":0.5,"MaxDistance":0.5,"Phone":"202-639-8700","AddressDescription":"1300 L St Nw\nWashington, DC 20005\nPlease consider tipping your driver for awesome service!!!","HolidaysDescription":"","HoursDescription":"Su-Th 10:00am-11:00pm\nFr-Sa 10:00am-12:00am","ServiceHoursDescription":{"Carryout":"Su-Sa 10:00am-10:00pm","Delivery":"Su-Th 10:00am-11:00pm\nFr-Sa 10:00am-12:00am","DriveUpCarryout":"Su-Sa 6:30pm-9:00pm"},"IsOnlineCapable":true,"IsOnlineNow":false,"IsNEONow":false,"IsSpanish":true,"LocationInfo":"Please consider tipping your driver for awesome service!!!","LanguageLocationInfo":{"es":"Please consider tipping your driver for awesome service!!!"},"AllowDeliveryOrders":true,"AllowCarryoutOrders":true,"AllowDuc":true,"ServiceMethodEstimatedWaitMinutes":{"Delivery":{"Min":13,"Max":23},"Carryout":{"Min":9,"Max":14}},"StoreCoordinates":{"StoreLatitude":"38.9036","StoreLongitude":"-77.03"},"AllowPickupWindowOrders":false,"ContactlessDelivery":"REQUIRED","ContactlessCarryout":"INSTRUCTION","IsOpen":false,"ServiceIsOpen":{"Carryout":false,"Delivery":false,"DriveUpCarryout":false}},{"StoreID":"4344","IsDeliveryStore":false,"MinDistance":0.6,"MaxDistance":0.6,"Phone":"202-223-1100","AddressDescription":"2029 K St Nw\nWashington, DC 20006","HolidaysDescription":"","HoursDescription":"Su-Th 10:00am-11:00pm\nFr-Sa 10:00am-12:00am","ServiceHoursDescription":{"Carryout":"Su-Sa 10:00am-9:45pm","Delivery":"Su-Th 10:00am-11:00pm\nFr-Sa 10:00am-12:00am","DriveUpCarryout":"Su-Sa 4:00pm-9:00pm"},"IsOnlineCapable":true,"IsOnlineNow":true,"IsNEONow":false,"IsSpanish":true,"LocationInfo":null,"LanguageLocationInfo":{},"AllowDeliveryOrders":true,"AllowCarryoutOrders":true,"AllowDuc":true,"ServiceMethodEstimatedWaitMinutes":{"Delivery":{"Min":20,"Max":30},"Carryout":{"Min":9,"Max":14}},"StoreCoordinates":{"StoreLatitude":"38.9026","StoreLongitude":"-77.0457"},"AllowPickupWindowOrders":false,"ContactlessDelivery":"REQUIRED","ContactlessCarryout":"INSTRUCTION","IsOpen":false,"ServiceIsOpen":{"Carryout":false,"Delivery":false,"DriveUpCarryout":false}},{"StoreID":"4328","IsDeliveryStore":false,"MinDistance":1.8,"MaxDistance":1.8,"Phone":"202-232-8400","AddressDescription":"2701 14 ST NW\nWashington, DC 20009\nALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up","HolidaysDescription":"","HoursDescription":"Su-Th 10:00am-12:00am\nFr-Sa 10:00am-1:00am","ServiceHoursDescription":{"Carryout":"Su-Sa 10:00am-9:45pm","Delivery":"Su-Th 10:00am-12:00am\nFr-Sa 10:00am-1:00am","DriveUpCarryout":"Su-Sa 4:00pm-9:00pm"},"IsOnlineCapable":true,"IsOnlineNow":true,"IsNEONow":false,"IsSpanish":true,"LocationInfo":"ALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up","LanguageLocationInfo":{"es":"ALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up"},"AllowDeliveryOrders":true,"AllowCarryoutOrders":true,"AllowDuc":true,"ServiceMethodEstimatedWaitMinutes":{"Delivery":{"Min":14,"Max":24},"Carryout":{"Min":8,"Max":13}},"StoreCoordinates":{"StoreLatitude":"38.924797","StoreLongitude":"-77.032249"},"AllowPickupWindowOrders":false,"ContactlessDelivery":"REQUIRED","ContactlessCarryout":"INSTRUCTION","IsOpen":false,"ServiceIsOpen":{"Carryout":false,"Delivery":false,"DriveUpCarryout":false}},{"StoreID":"4329","IsDeliveryStore":false,"MinDistance":1.9,"MaxDistance":1.9,"Phone":"202-526-8600","AddressDescription":"1335 2nd street NE\nWashington, DC 20002\nALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up","HolidaysDescription":"","HoursDescription":"Su-Th 10:00am-12:00am\nFr-Sa 10:00am-1:00am","ServiceHoursDescription":{"Carryout":"Su-Sa 10:00am-9:45pm","Delivery":"Su-Th 10:00am-12:00am\nFr-Sa 10:00am-1:00am","DriveUpCarryout":"Su-Sa 4:00pm-9:00pm"},"IsOnlineCapable":true,"IsOnlineNow":true,"IsNEONow":false,"IsSpanish":true,"LocationInfo":"ALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up","LanguageLocationInfo":{"es":"ALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up"},"AllowDeliveryOrders":true,"AllowCarryoutOrders":true,"AllowDuc":true,"ServiceMethodEstimatedWaitMinutes":{"Delivery":{"Min":13,"Max":23},"Carryout":{"Min":8,"Max":13}},"StoreCoordinates":{"StoreLatitude":"38.908243","StoreLongitude":"-77.003327"},"AllowPickupWindowOrders":false,"ContactlessDelivery":"REQUIRED","ContactlessCarryout":"INSTRUCTION","IsOpen":false,"ServiceIsOpen":{"Carryout":false,"Delivery":false,"DriveUpCarryout":false}},{"StoreID":"4330","IsDeliveryStore":false,"MinDistance":2.5,"MaxDistance":2.5,"Phone":"202-342-0100","AddressDescription":"2330 Wisconsin Ave NW\nWashington, DC 20007\nWE HAVE MOVED THIS IS A NEW LOCATION","HolidaysDescription":"","HoursDescription":"Su-Th 10:00am-11:00pm\nFr-Sa 10:00am-12:00am","ServiceHoursDescription":{"Carryout":"Su-Sa 10:00am-9:45pm","Delivery":"Su-Th 10:00am-11:00pm\nFr-Sa 10:00am-12:00am","DriveUpCarryout":"Su-Sa 4:00pm-9:00pm"},"IsOnlineCapable":true,"IsOnlineNow":true,"IsNEONow":false,"IsSpanish":true,"LocationInfo":"WE HAVE MOVED THIS IS A NEW LOCATION","LanguageLocationInfo":{"es":"ALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up"},"AllowDeliveryOrders":true,"AllowCarryoutOrders":true,"AllowDuc":true,"ServiceMethodEstimatedWaitMinutes":{"Delivery":{"Min":13,"Max":23},"Carryout":{"Min":9,"Max":14}},"StoreCoordinates":{"StoreLatitude":"38.920699","StoreLongitude":"-77.072488"},"AllowPickupWindowOrders":false,"ContactlessDelivery":"REQUIRED","ContactlessCarryout":"INSTRUCTION","IsOpen":false,"ServiceIsOpen":{"Carryout":false,"Delivery":false,"DriveUpCarryout":false}},{"StoreID":"4326","IsDeliveryStore":false,"MinDistance":2.8,"MaxDistance":2.8,"Phone":"202-484-3030","AddressDescription":"900 M St SE\nWashington, DC 20003\nPlease consider tipping your driver for awesome service!!!","HolidaysDescription":"","HoursDescription":"Su-Th 10:00am-12:00am\nFr-Sa 10:00am-1:00am","ServiceHoursDescription":{"Carryout":"Su-Sa 10:00am-10:00pm","Delivery":"Su-Th 10:00am-12:00am\nFr-Sa 10:00am-1:00am","DriveUpCarryout":"Su-Sa 4:00pm-9:00pm"},"IsOnlineCapable":true,"IsOnlineNow":true,"IsNEONow":false,"IsSpanish":true,"LocationInfo":"Please consider tipping your driver for awesome service!!!","LanguageLocationInfo":{"es":"Please consider tipping your driver for awesome service!!!"},"AllowDeliveryOrders":true,"AllowCarryoutOrders":true,"AllowDuc":true,"ServiceMethodEstimatedWaitMinutes":{"Delivery":{"Min":15,"Max":25},"Carryout":{"Min":9,"Max":14}},"StoreCoordinates":{"StoreLatitude":"38.876478","StoreLongitude":"-76.993744"},"AllowPickupWindowOrders":false,"ContactlessDelivery":"REQUIRED","ContactlessCarryout":"INSTRUCTION","IsOpen":false,"ServiceIsOpen":{"Carryout":false,"Delivery":false,"DriveUpCarryout":false}},{"StoreID":"4335","IsDeliveryStore":false,"MinDistance":2.8,"MaxDistance":2.8,"Phone":"202-832-3343","AddressDescription":"208 Michigan Ave NE\nWashington, DC 20011\nALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up","HolidaysDescription":"","HoursDescription":"Su-Th 10:00am-12:00am\nFr-Sa 10:00am-1:00am","ServiceHoursDescription":{"Carryout":"Su-Sa 10:00am-9:45pm","Delivery":"Su-Th 10:00am-12:00am\nFr-Sa 10:00am-1:00am","DriveUpCarryout":"Su-Sa 4:00pm-9:00pm"},"IsOnlineCapable":true,"IsOnlineNow":true,"IsNEONow":false,"IsSpanish":true,"LocationInfo":"ALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up","LanguageLocationInfo":{"es":"ALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up"},"AllowDeliveryOrders":true,"AllowCarryoutOrders":true,"AllowDuc":true,"ServiceMethodEstimatedWaitMinutes":{"Delivery":{"Min":29,"Max":39},"Carryout":{"Min":9,"Max":14}},"StoreCoordinates":{"StoreLatitude":"38.930284","StoreLongitude":"-77.002642"},"AllowPickupWindowOrders":false,"ContactlessDelivery":"REQUIRED","ContactlessCarryout":"INSTRUCTION","IsOpen":false,"ServiceIsOpen":{"Carryout":false,"Delivery":false,"DriveUpCarryout":false}},{"StoreID":"4341","IsDeliveryStore":false,"MinDistance":3.6,"MaxDistance":3.6,"Phone":"703-521-3030","AddressDescription":"2602 Columbia Pike\nArlington, VA 22204\nDUE TO COVID19 DRIVER WILL NOT GO INSIDE ANY APARTMENT.. LOBBY ONLY!!","HolidaysDescription":"","HoursDescription":"Su-Th 10:00am-12:00am\nFr-Sa 10:00am-1:00am","ServiceHoursDescription":{"Carryout":"Su-Sa 10:00am-9:00pm","Delivery":"Su-Th 10:00am-12:00am\nFr-Sa 10:00am-1:00am","DriveUpCarryout":"Su-Sa 5:00pm-9:00pm"},"IsOnlineCapable":true,"IsOnlineNow":true,"IsNEONow":false,"IsSpanish":true,"LocationInfo":"DUE TO COVID19 DRIVER WILL NOT GO INSIDE ANY APARTMENT.. LOBBY ONLY!!","LanguageLocationInfo":{"es":""},"AllowDeliveryOrders":true,"AllowCarryoutOrders":true,"AllowDuc":true,"ServiceMethodEstimatedWaitMinutes":{"Delivery":{"Min":15,"Max":25},"Carryout":{"Min":9,"Max":14}},"StoreCoordinates":{"StoreLatitude":"38.8629","StoreLongitude":"-77.0853"},"AllowPickupWindowOrders":false,"ContactlessDelivery":"REQUIRED","ContactlessCarryout":"INSTRUCTION","IsOpen":false,"ServiceIsOpen":{"Carryout":false,"Delivery":false,"DriveUpCarryout":false}},{"StoreID":"4346","IsDeliveryStore":false,"MinDistance":3.9,"MaxDistance":3.9,"Phone":"703-684-3344","AddressDescription":"3535 SOUTH BALL ST\nArlington, VA 22202\nDUE TO COVID19 DRIVER WILL NOT GO INSIDE ANY APARTMENT.. LOBBY ONLY!!","HolidaysDescription":"","HoursDescription":"Su-Th 10:00am-12:00am\nFr-Sa 10:00am-1:00am","ServiceHoursDescription":{"Carryout":"Su-Sa 10:00am-9:00pm","Delivery":"Su-Th 10:00am-11:00pm\nFr-Sa 12:00am-12:00am,10:00am-12:00am","DriveUpCarryout":"Su-Sa 4:00pm-9:00pm"},"IsOnlineCapable":true,"IsOnlineNow":false,"IsNEONow":false,"IsSpanish":true,"LocationInfo":"DUE TO COVID19 DRIVER WILL NOT GO INSIDE ANY APARTMENT.. LOBBY ONLY!!","LanguageLocationInfo":{"es":""},"AllowDeliveryOrders":true,"AllowCarryoutOrders":true,"AllowDuc":true,"ServiceMethodEstimatedWaitMinutes":{},"StoreCoordinates":{"StoreLatitude":"38.84315","StoreLongitude":"-77.052102"},"AllowPickupWindowOrders":false,"ContactlessDelivery":"REQUIRED","ContactlessCarryout":"INSTRUCTION","IsOpen":false,"ServiceIsOpen":{"Carryout":false,"Delivery":false,"DriveUpCarryout":false}},{"StoreID":"4333","IsDeliveryStore":false,"MinDistance":4.1,"MaxDistance":4.1,"Phone":"703-276-1400","AddressDescription":"550 North Quincy St\nArlington, VA 22203\nDUE TO COVID19 DRIVER WILL NOT GO INSIDE ANY APT... LOBBY ONLY","HolidaysDescription":"","HoursDescription":"Su-Th 10:00am-12:00am\nFr-Sa 10:00am-1:00am","ServiceHoursDescription":{"Carryout":"Su-Sa 10:00am-9:00pm","Delivery":"Su-Th 10:00am-12:00am\nFr-Sa 10:00am-1:00am","DriveUpCarryout":"Su-Sa 5:00pm-9:00pm"},"IsOnlineCapable":true,"IsOnlineNow":true,"IsNEONow":false,"IsSpanish":true,"LocationInfo":"DUE TO COVID19 DRIVER WILL NOT GO INSIDE ANY APT... LOBBY ONLY","LanguageLocationInfo":{"es":""},"AllowDeliveryOrders":true,"AllowCarryoutOrders":true,"AllowDuc":true,"ServiceMethodEstimatedWaitMinutes":{"Delivery":{"Min":15,"Max":25},"Carryout":{"Min":9,"Max":14}},"StoreCoordinates":{"StoreLatitude":"38.878103","StoreLongitude":"-77.1081"},"AllowPickupWindowOrders":false,"ContactlessDelivery":"REQUIRED","ContactlessCarryout":"INSTRUCTION","IsOpen":false,"ServiceIsOpen":{"Carryout":false,"Delivery":false,"DriveUpCarryout":false}},{"StoreID":"4331","IsDeliveryStore":false,"MinDistance":4.2,"MaxDistance":4.2,"Phone":"202-362-7500","AddressDescription":"4539 Wisconsin Ave Nw\nWashington, DC 20016\nALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up","HolidaysDescription":"","HoursDescription":"Su-Th 10:00am-11:00pm\nFr-Sa 10:00am-12:00am","ServiceHoursDescription":{"Carryout":"Su-Sa 10:00am-9:45pm","Delivery":"Su-Th 10:00am-11:00pm\nFr-Sa 10:00am-12:00am","DriveUpCarryout":"Su-Sa 4:00pm-9:00pm"},"IsOnlineCapable":true,"IsOnlineNow":true,"IsNEONow":true,"IsSpanish":true,"LocationInfo":"ALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up","LanguageLocationInfo":{"es":"ALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up"},"AllowDeliveryOrders":true,"AllowCarryoutOrders":true,"AllowDuc":true,"ServiceMethodEstimatedWaitMinutes":{"Delivery":{"Min":18,"Max":28},"Carryout":{"Min":9,"Max":14}},"StoreCoordinates":{"StoreLatitude":"38.949085","StoreLongitude":"-77.080234"},"AllowPickupWindowOrders":false,"ContactlessDelivery":"REQUIRED","ContactlessCarryout":"INSTRUCTION","IsOpen":false,"ServiceIsOpen":{"Carryout":false,"Delivery":false,"DriveUpCarryout":false}},{"StoreID":"4362","IsDeliveryStore":false,"MinDistance":4.7,"MaxDistance":4.7,"Phone":"202-291-6100","AddressDescription":"6239 Georgia Ave\nWashington, DC 20011\nALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up","HolidaysDescription":"","HoursDescription":"Su-Th 10:00am-12:00am\nFr-Sa 10:00am-1:00am","ServiceHoursDescription":{"Carryout":"Su-Sa 10:00am-9:45pm","Delivery":"Su-Th 10:00am-12:00am\nFr-Sa 10:00am-1:00am","DriveUpCarryout":"Su-Sa 4:00pm-9:00pm"},"IsOnlineCapable":true,"IsOnlineNow":true,"IsNEONow":false,"IsSpanish":true,"LocationInfo":"ALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up","LanguageLocationInfo":{"es":"ALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up"},"AllowDeliveryOrders":true,"AllowCarryoutOrders":true,"AllowDuc":true,"ServiceMethodEstimatedWaitMinutes":{"Delivery":{"Min":13,"Max":23},"Carryout":{"Min":8,"Max":13}},"StoreCoordinates":{"StoreLatitude":"38.965922","StoreLongitude":"-77.027331"},"AllowPickupWindowOrders":false,"ContactlessDelivery":"REQUIRED","ContactlessCarryout":"INSTRUCTION","IsOpen":false,"ServiceIsOpen":{"Carryout":false,"Delivery":false,"DriveUpCarryout":false}},{"StoreID":"4339","IsDeliveryStore":false,"MinDistance":4.8,"MaxDistance":4.8,"Phone":"703-243-0004","AddressDescription":"4811 Lee Hwy\nArlington, VA 22207\nDUE TO COVID19 DRIVER WILL NOT GO INSIDE ANY APARTMENT.. LOBBY ONLY!!!","HolidaysDescription":"","HoursDescription":"Su-Th 10:00am-12:00am\nFr-Sa 10:00am-1:00am","ServiceHoursDescription":{"Carryout":"Su-Sa 10:00am-9:00pm","Delivery":"Su-Th 10:00am-12:00am\nFr-Sa 10:00am-1:00am","DriveUpCarryout":"Su-Sa 5:00pm-9:00pm"},"IsOnlineCapable":true,"IsOnlineNow":false,"IsNEONow":false,"IsSpanish":true,"SubstitutionStore":"4339","LocationInfo":"DUE TO COVID19 DRIVER WILL NOT GO INSIDE ANY APARTMENT.. LOBBY ONLY!!!","LanguageLocationInfo":{"es":""},"AllowDeliveryOrders":true,"AllowCarryoutOrders":true,"AllowDuc":true,"ServiceMethodEstimatedWaitMinutes":{},"StoreCoordinates":{"StoreLatitude":"38.897","StoreLongitude":"-77.1254"},"AllowPickupWindowOrders":false,"ContactlessDelivery":"REQUIRED","ContactlessCarryout":"INSTRUCTION","IsOpen":false,"ServiceIsOpen":{"Carryout":false,"Delivery":false,"DriveUpCarryout":false}}]} \ No newline at end of file diff --git a/dawg/testdata/store.json b/dawg/testdata/store.json new file mode 100644 index 0000000..dcbfcfc --- /dev/null +++ b/dawg/testdata/store.json @@ -0,0 +1 @@ +{"AcceptAnonymousCreditCards":true,"AcceptGiftCards":true,"AcceptSavedCreditCard":true,"AcceptableCreditCards":["American Express","Discover Card","Mastercard","Optima","Visa"],"AcceptablePaymentTypes":["Cash","GiftCard","CreditCard"],"AcceptableTipPaymentTypes":["CreditCard"],"AcceptableWalletTypes":["Google"],"AddressDescription":"2701 14 ST NW\nWashington, DC 20009\nALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up","AdvDelDash":false,"AllowAutonomousDelivery":false,"AllowCardSaving":true,"AllowCarryoutOrders":true,"AllowDeliveryOrders":true,"AllowDineInOrders":false,"AllowDriverPooling":false,"AllowDuc":false,"AllowDynamicDeliveryFees":false,"AllowPickupWindowOrders":false,"AllowPiePass":true,"AllowRemoteDispatch":false,"AllowSmsNotification":true,"AlternatePaymentProcess":false,"AsOfTime":"2020-04-08 15:29:46","BusinessDate":"2020-04-08","CarryoutWaitTimeReason":null,"CashLimit":50,"City":"Washington","ContactlessCarryout":"INSTRUCTION","ContactlessDelivery":"AVAILABLE","CustomerCloseWarningMinutes":30,"DeliveryWaitTimeReason":null,"DriverTrackingSupportMode":"NOLO_VISIBLE","DriverTrackingSupported":"true","EstimatedWaitMinutes":"14-24","FutureOrderBlackoutBusinessDate":null,"FutureOrderDelayInHours":1,"HasKiosk":false,"Holidays":{"2020-04-08":{"Hours":[{"CloseTime":"22:59","OpenTime":"10:00"}]},"2020-04-09":{"Hours":[{"CloseTime":"22:59","OpenTime":"10:00"}]},"2020-04-10":{"Hours":[{"CloseTime":"23:59","OpenTime":"10:00"}]},"2020-04-11":{"Hours":[{"CloseTime":"23:59","OpenTime":"10:00"}]},"2020-04-12":{"Hours":[{"CloseTime":"22:59","OpenTime":"10:00"}]}},"HolidaysDescription":"04/08 10:00am-11:00pm\n04/09 10:00am-11:00pm\n04/10 10:00am-12:00am\n04/11 10:00am-12:00am\n04/12 10:00am-11:00pm","Hours":{"Fri":[{"CloseTime":"23:59","OpenTime":"10:00"}],"Mon":[{"CloseTime":"22:59","OpenTime":"10:00"}],"Sat":[{"CloseTime":"23:59","OpenTime":"10:00"}],"Sun":[{"CloseTime":"22:59","OpenTime":"10:00"}],"Thu":[{"CloseTime":"22:59","OpenTime":"10:00"}],"Tue":[{"CloseTime":"22:59","OpenTime":"10:00"}],"Wed":[{"CloseTime":"22:59","OpenTime":"10:00"}]},"HoursDescription":"Su-Th 10:00am-11:00pm\nFr-Sa 10:00am-12:00am","IsAVSEnabled":true,"IsAffectedByDaylightSavingsTime":true,"IsAllergenWarningEnabled":false,"IsCookingInstructionsEnabled":false,"IsDriverSafetyEnabled":false,"IsForceClose":false,"IsForceOffline":false,"IsNEONow":false,"IsOnlineCapable":true,"IsOnlineNow":true,"IsOpen":true,"IsSaltWarningEnabled":false,"IsSpanish":true,"IsTippingAllowedAtCheckout":true,"LanguageLocationInfo":{"es":"ALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up"},"LanguageTranslations":{"en":{"LocationInfo":"ALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up"},"es":{"LocationInfo":"ALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up","StoreName":""}},"LocationInfo":"ALL Credit Card orders must have Credit Card and ID present at the Time of Delivery or Pick-up","MarketPaymentTypes":[],"MinimumDeliveryOrderAmount":15.48,"OnlineStatusCode":"Ok","OptInAAA":true,"Phone":"202-232-8400","Pop":true,"PostalCode":"20009","PreferredCurrency":"USD","PreferredLanguage":"en-US","PulseVersion":"6.89.321","PulseVersionName":"3.89","RawPaymentGateway":"1","Region":"DC","SaltWarningInfo":null,"ServiceHours":{"Carryout":{"Fri":[{"CloseTime":"21:45","OpenTime":"10:00"}],"Mon":[{"CloseTime":"21:45","OpenTime":"10:00"}],"Sat":[{"CloseTime":"21:45","OpenTime":"10:00"}],"Sun":[{"CloseTime":"21:45","OpenTime":"10:00"}],"Thu":[{"CloseTime":"21:45","OpenTime":"10:00"}],"Tue":[{"CloseTime":"21:45","OpenTime":"10:00"}],"Wed":[{"CloseTime":"21:45","OpenTime":"10:00"}]},"Delivery":{"Fri":[{"CloseTime":"23:59","OpenTime":"10:00"}],"Mon":[{"CloseTime":"22:59","OpenTime":"10:00"}],"Sat":[{"CloseTime":"23:59","OpenTime":"10:00"}],"Sun":[{"CloseTime":"22:59","OpenTime":"10:00"}],"Thu":[{"CloseTime":"22:59","OpenTime":"10:00"}],"Tue":[{"CloseTime":"22:59","OpenTime":"10:00"}],"Wed":[{"CloseTime":"22:59","OpenTime":"10:00"}]},"DriveUpCarryout":{"Fri":[{"CloseTime":"20:59","OpenTime":"16:00"}],"Mon":[{"CloseTime":"20:59","OpenTime":"16:00"}],"Sat":[{"CloseTime":"20:59","OpenTime":"16:00"}],"Sun":[{"CloseTime":"20:59","OpenTime":"16:00"}],"Thu":[{"CloseTime":"20:59","OpenTime":"16:00"}],"Tue":[{"CloseTime":"20:59","OpenTime":"16:00"}],"Wed":[{"CloseTime":"20:59","OpenTime":"16:00"}]}},"ServiceHoursDescription":{"Carryout":"Su-Sa 10:00am-9:45pm","Delivery":"Su-Th 10:00am-11:00pm\nFr-Sa 10:00am-12:00am","DriveUpCarryout":"Su-Sa 4:00pm-9:00pm"},"ServiceMethodEstimatedWaitMinutes":{"Carryout":{"Max":15,"Min":10},"Delivery":{"Max":24,"Min":14}},"SocialReviewLinks":{"gmb":"http://search.google.com/local/writereview?placeid=ChIJoxUp5-e3t4kR6LQ1sddDy6Q","plus":"https://plus.google.com/104470678873051810563","yelp":"http://www.yelp.com/biz/RX8OJ2y4q48VfIiDm83WuQ"},"Status":0,"StoreAsOfTime":"2020-04-08 15:29:11","StoreCoordinates":{"StoreLatitude":"38.924797","StoreLongitude":"-77.032249"},"StoreEndTimeEvenSpansToNextBusinessDay":"2020-04-08 22:59:00","StoreID":"4328","StoreLocation":{"Latitude":"38.924797","Longitude":"-77.032249"},"StoreName":"","StoreVariance":null,"StreetName":"2701 14 ST NW","TimeZoneCode":"GMT-04:00","TimeZoneMinutes":-240,"Tokenization":true,"Upsell":{},"ecomActive":true} \ No newline at end of file diff --git a/dawg/user.go b/dawg/user.go index cd89614..a3f0bc4 100644 --- a/dawg/user.go +++ b/dawg/user.go @@ -3,52 +3,90 @@ package dawg import ( "errors" "fmt" + "net/http" + "net/url" "strconv" "strings" + "time" ) // SignIn will create a new UserProfile and sign in the account. func SignIn(username, password string) (*UserProfile, error) { - a, err := newauth(username, password) + err := authorize(orderClient.Client, username, password) if err != nil { return nil, err } - return a.login() + return login(orderClient) } +// TODO: find out how to update a profile on domino's end + // UserProfile is a Dominos user profile. type UserProfile struct { - FirstName string - LastName string - Email string - CustomerID string - Phone string - Addresses []*UserAddress + FirstName string + LastName string + Phone string + + // Type of dominos account + Type string + // Dominos internal user id + ID string `json:"CustomerID"` + // Identifiers are the pieces of information used to identify the + // user (even if they are not signed in) + Identifiers []string `json:"CustomerIdentifiers"` + // AgreedToTermsOfUse is true if the user has agreed to dominos terms of use. + AgreedToTermsOfUse bool `json:"AgreeToTermsOfUse"` + // User's gender + Gender string + // List of all the addresses saved in the dominos account + Addresses []*UserAddress + + // Email is the user's email address + Email string + // EmailOptIn tells wether the user is opted in for email updates or not + EmailOptIn bool + // EmailOptInTime shows what time the user last opted in for email updates + EmailOptInTime string + + // SmsPhone is the phone number used for sms updates + SmsPhone string + // SmsOptIn tells wether the use is opted for sms updates or not + SmsOptIn bool + // SmsOptInTime shows the last time the user opted in for sms updates + SmsOptInTime string + + // UpdateTime shows the last time the user's profile was updated + UpdateTime string // ServiceMethod should be "Delivery" or "Carryout" - ServiceMethod string `json:"-"` + ServiceMethod string `json:"-"` // this is a package specific field (not from the api) - auth *auth - store *Store + ordersMeta *customerOrders + cli *client + store *Store + loyaltyData *CustomerLoyalty } // AddAddress will add an address to the dominos account. func (u *UserProfile) AddAddress(a Address) { // TODO: consider sending a request to dominos to update the user with this address. - // this can be done in a separate go-routine u.Addresses = append(u.Addresses, UserAddressFromAddress(a)) } -var errNoServiceMethod = errors.New("no service method given") +var ( + errNoServiceMethod = errors.New("no service method given") + errUserNoServiceMethod = errors.New("User has no ServiceMethod set") +) // StoresNearMe will find the stores closest to the user's default address. func (u *UserProfile) StoresNearMe() ([]*Store, error) { if u.ServiceMethod == "" { - return nil, errNoServiceMethod + return nil, errUserNoServiceMethod } - - address := u.DefaultAddress() - return asyncNearbyStores(u.auth.cli, address, u.ServiceMethod) + if err := u.addressCheck(); err != nil { + return nil, err + } + return asyncNearbyStores(u.cli, u.DefaultAddress(), u.ServiceMethod) } // NearestStore will find the the store that is closest to the user's default address. @@ -58,10 +96,13 @@ func (u *UserProfile) NearestStore(service string) (*Store, error) { return u.store, nil } - // pass the authorized user's client along to - // the store which will use the user's credentitals + // Pass the authorized user's client along to the + // store which will use the user's credentials // on each request. - c := &client{host: orderHost, Client: u.auth.cli.Client} + c := &client{host: orderHost, Client: u.cli.Client} + if err = u.addressCheck(); err != nil { + return nil, err + } u.store, err = getNearestStore(c, u.DefaultAddress(), service) return u.store, err } @@ -92,37 +133,173 @@ func (u *UserProfile) SetServiceMethod(service string) error { return nil } +// SetStore will set the UserProfile struct's internal store variable. +func (u *UserProfile) SetStore(store *Store) error { + if store == nil { + return errors.New("cannot set UserProfile store to a nil value") + } + if store.ID == "" { + return errors.New("UserProfile.SetStore: store is uninitialized") + } + u.store = store + return nil +} + +// TODO: write tests for GetCards, Loyalty, PreviousOrders, GetEasyOrder, initOrdersMeta, and customerEndpoint + +// Cards will get the cards that Dominos has saved in their database. (see UserCard) +func (u *UserProfile) Cards() ([]*UserCard, error) { + cards := make([]*UserCard, 0) + return cards, u.customerEndpoint(u.cli, "card", nil, &cards) +} + +// Loyalty returns the user's loyalty meta-data (see CustomerLoyalty) +func (u *UserProfile) Loyalty() (*CustomerLoyalty, error) { + u.loyaltyData = new(CustomerLoyalty) + return u.loyaltyData, u.customerEndpoint(u.cli, "loyalty", nil, u.loyaltyData) +} + +// for internal use (caches the loyalty data) +func (u *UserProfile) getLoyalty() (*CustomerLoyalty, error) { + if u.loyaltyData != nil { + return u.loyaltyData, nil + } + return u.Loyalty() +} + +// PreviousOrders will return `n` of the user's previous orders. +func (u *UserProfile) PreviousOrders(n int) ([]*EasyOrder, error) { + return u.ordersMeta.CustomerOrders, u.initOrdersMeta(n) +} + +// GetEasyOrder will return the user's easy order. +func (u *UserProfile) GetEasyOrder() (*EasyOrder, error) { + var err error + if u.ordersMeta == nil { + if err = u.initOrdersMeta(3); err != nil { + return nil, err + } + } + return u.ordersMeta.EasyOrder, nil +} + +// Orders returns a variety of meta-data on the user's previous and saved orders. +func (u *UserProfile) initOrdersMeta(limit int) error { + u.ordersMeta = &customerOrders{} + return u.customerEndpoint( + u.cli, "order", + Params{"limit": limit, "lang": DefaultLang}, + &u.ordersMeta, + ) +} + +// NewOrder will create a new *dawg.Order struct with all of the user's information. +func (u *UserProfile) NewOrder() (*Order, error) { + var err error + if u.store == nil { + _, err = u.NearestStore(u.ServiceMethod) + if err != nil { + return nil, err + } + } + order := &Order{ + FirstName: u.FirstName, + LastName: u.LastName, + Email: u.Email, + LanguageCode: DefaultLang, + ServiceMethod: u.ServiceMethod, + StoreID: u.store.ID, + CustomerID: u.ID, + Phone: u.Phone, + Products: []*OrderProduct{}, + Address: StreetAddrFromAddress(u.store.userAddress), + Payments: []*orderPayment{}, + cli: u.cli, + } + return order, nil +} + +// returns and error if the user has no address +// +// addressCheck is meant to be a check before DefaultAddress is called internally. +func (u *UserProfile) addressCheck() error { + if len(u.Addresses) == 0 { + return errors.New("UserProfile has no addresses") + } + return nil +} + +func (u *UserProfile) serviceCheck() error { + if u.ServiceMethod == "" { + return ErrNoUserService + } + return nil +} + +func (u *UserProfile) customerEndpoint( + d doer, + path string, + params Params, + obj interface{}, +) error { + if u.ID == "" { + return errors.New("UserProfile not fully initialized: needs CustomerID") + } + if params == nil { + params = make(Params) + } + params["_"] = time.Now().Nanosecond() + + return dojson(d, obj, &http.Request{ + Method: "GET", + Proto: "HTTP/1.1", + Header: make(http.Header), + URL: &url.URL{ + Scheme: "https", + Host: orderHost, + Path: fmt.Sprintf("/power/customer/%s/%s", u.ID, path), + RawQuery: params.Encode(), + }, + }) +} + // UserAddress is an address that is saved by dominos and returned when // a user signs in. type UserAddress struct { - // TODO: find out which of these fields are not needed - AddressType string - StreetNumber string - StreetRange string - AddressLine2 string - PropertyType string - StreetField2 string - LocationName string - SubNeighborhood string - StreetField1 string - UnitNumber string - AddressLine4 string - PostalCode string - BuildingID string - IsDefault bool - UpdateTime string - PropertyNumber string - UnitType string - Coordinates map[string]float32 - Neighborhood string - Street string - CityName string `json:"City"` - Region string + // Basic address fields + Street string + StreetName string + StreetNumber string + CityName string `json:"City"` + Region string + PostalCode string + AddressType string + + // Dominos specific meta-data Name string - StreetName string + IsDefault bool DeliveryInstructions string - AddressLine3 string - CampusID string + UpdateTime string + + // Other address specific fields + AddressLine2 string + AddressLine3 string + AddressLine4 string + StreetField1 string + StreetField2 string + StreetRange string + + // Other rarely-used address meta-data fields + PropertyType string + PropertyNumber string + UnitType string + UnitNumber string + BuildingID string + CampusID string + Neighborhood string + SubNeighborhood string + LocationName string + Coordinates map[string]float32 } var _ Address = (*UserAddress)(nil) @@ -179,3 +356,109 @@ func (ua *UserAddress) StateCode() string { func (ua *UserAddress) Zip() string { return ua.PostalCode } + +// UserCard holds the card data that Dominos stores and send back to users. +// For security reasons, Dominos does not send the raw card number or the +// raw security code. Insted they send a card ID that is used to reference +// that card. This allows users to reference that card using the card's +// nickname (see 'NickName' field) +type UserCard struct { + // ID is the card id used by dominos internally to reference a user's + // card without sending the actual card number over the internet + ID string `json:"id"` + // NickName is the cards name, given when a user saves a card as a named card + NickName string `json:"nickName"` + IsDefault bool `json:"isDefault"` + + TimesCharged int `json:"timesCharged"` + TimesChargedIsValid bool `json:"timesChargedIsValid"` + + // LastFour is a field that gives the last four didgets of the card number + LastFour string `json:"lastFour"` + // true if the card has expired + IsExpired bool `json:"isExpired"` + ExpirationMonth int `json:"expirationMonth"` + ExpirationYear int `json:"expirationYear"` + // LastUpdated shows the date that this card was last updated in + // dominos databases + LastUpdated string `json:"lastUpdated"` + + CardType string `json:"cardType"` + BillingZip string `json:"billingZip"` +} + +// CustomerLoyalty is a struct that holds account meta-data used by +// Dominos to keep track of customer rewards. +type CustomerLoyalty struct { + CustomerID string + EnrollDate string + LastActivityDate string + BasePointExpirationDate string + PendingPointBalance string + AccountStatus string + // VestedPointBalance is the points you have + // saved up in order to get a free pizza. + VestedPointBalance int + // This is a list of possible coupons that a + // customer can receive. + LoyaltyCoupons []struct { + CouponCode string + PointValue int + BaseCoupon bool + LimitPerOrder string + } +} + +// TODO: figure out how the dominos website sends an easy order to the servers + +type customerOrders struct { + CustomerOrders []*EasyOrder `json:"customerOrders"` + EasyOrder *EasyOrder `json:"easyOrder"` + Products map[string]OrderProduct `json:"products"` + + ProductsByFrequencyRecency []struct { + ProductKey string `json:"productKey"` + Frequency int `json:"frequency"` + } `json:"productsByFrequencyRecency"` + + ProductsByCategory []struct { + Category string `json:"category"` + ProductKeys []string `json:"productKeys"` + } `json:"productsByCategory"` +} + +// EasyOrder is an easy order. +type EasyOrder struct { + AddressNickName string `json:"addressNickName"` + OrderNickName string `json:"easyOrderNickName"` + EasyOrder bool `json:"easyOrder"` + ID string `json:"id"` + DeliveryInstructions string `json:"deliveryInstructions"` + Cards []struct { + ID string `json:"id"` + NickName string `json:"nickName"` + } + + Store struct { + Address *StreetAddr `json:"address"` + CarryoutServiceHours string `json:"carryoutServiceHours"` + DeliveryServiceHours string `json:"deliveryServiceHours"` + } `json:"store"` + Order previousOrder `json:"order"` +} + +type previousOrder struct { + Order + pricedOrder + + Partners interface{} + StoreOrderID string + StorePlaceOrderTime string + OrderMethod string + IP string + + PlaceOrderTime string // YYYY-MM-DD H:M:S + BusinessDate string // YYYY-MM-DD + + OrderInfoCollection []interface{} +} diff --git a/dawg/user_test.go b/dawg/user_test.go index 1b5aca3..922c80b 100644 --- a/dawg/user_test.go +++ b/dawg/user_test.go @@ -1,26 +1,243 @@ package dawg import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "sync" "testing" + + "github.com/harrybrwn/apizza/dawg/internal/auth" + "github.com/harrybrwn/apizza/pkg/tests" ) -func TestUserNearestStore(t *testing.T) { - uname, pass, ok := gettestcreds() +func TestSignIn(t *testing.T) { + client, mux, server := testServer() + defer server.Close() + defer swapClientWith(client)() + addUserHandlers(t, mux) + username, password, ok := gettestcreds() if !ok { t.Skip() } - defer swapclient(10)() + tests.InitHelpers(t) - user, err := getTestUser(uname, pass) + user, err := getTestUser(username, password) // calls SignIn if global user is nil + tests.Check(err) + tests.NotNil(user) + user, err = SignIn("blah", "blahblah") + tests.Exp(err) + if user != nil { + t.Errorf("expected nil %T", user) + } +} + +func TestUser_WithProxy(t *testing.T) { + client, mux, server := testServer() + defer server.Close() + defer swapClientWith(client)() + addUserHandlers(t, mux) + uname, pass, _ := gettestcreds() + user, err := SignIn(uname, pass) if err != nil { t.Error(err) } if user == nil { - t.Fatal("user is nil") + t.Fatal("nil user") + } + if user.ID != "123" { + t.Error("wrong id") + } + if user.cli.Client.Transport.(*auth.Token).ExpiresIn != 42069 { + t.Error("wrong expiration number") + } +} + +func TestUser(t *testing.T) { + client, mux, server := testServer() + defer server.Close() + defer swapClientWith(client)() + addUserHandlers(t, mux) + mux.HandleFunc("/power/store-locator", storeLocatorHandlerFunc(t)) + mux.HandleFunc("/power/store/", storeProfileHandlerFunc(t)) + + username, password, _ := gettestcreds() + user, err := SignIn(username, password) + if err != nil { + t.Error(err) + } + user.AddAddress(&UserAddress{ + Street: "1600 Pennsylvania Ave NW", + CityName: "Washington", + Region: "DC", + PostalCode: "20500", + AddressType: "House", + }) + if !testAddress().Equal(user.Addresses[0]) { + t.Error("addresses should be equal") + } + user.SetServiceMethod(Delivery) + + t.Run("User.NearestStore", func(t *testing.T) { + store, err := user.NearestStore(Delivery) + if err != nil { + t.Error(err) + } + if store.cli == nil { + t.Error("no client on store") + } + if store.ID == "" { + t.Error("no id") + } + }) + t.Run("User.StoresNearMe", func(t *testing.T) { + store, err := user.StoresNearMe() + if err != nil { + t.Error(err) + } + if len(store) == 0 { + t.Error("got no stores") + } + }) + t.Run("User.SetStore", func(t *testing.T) { + s := user.store + err := user.SetStore(nil) + if err == nil { + t.Error("expected an error") + } + err = user.SetStore(&Store{ID: ""}) + if err == nil { + t.Error("expected an error") + } + err = user.SetStore(&Store{ID: "1234"}) + if err != nil { + t.Error(err) + } + user.store = s + }) +} + +func TestUser_CustmerEndpoint(t *testing.T) { + client, mux, server := testServer() + defer server.Close() + defer swapClientWith(client)() + addUserHandlers(t, mux) + mux.HandleFunc("/power/store-locator", storeLocatorHandlerFunc(t)) + mux.HandleFunc("/power/store/", storeProfileHandlerFunc(t)) + mux.HandleFunc("/power/customer/", func(w http.ResponseWriter, r *http.Request) { + fileHandleFunc(t, "./testdata/order-meta.json")(w, r) + }) + + username, password, _ := gettestcreds() + user, err := SignIn(username, password) + if err != nil { + t.Error(err) + } + user.AddAddress(testAddress()) + order, err := user.GetEasyOrder() + if err != nil { + t.Error(err) + } + if order == nil { + t.Error("got nil easy order") + } + if _, err = user.getLoyalty(); err != nil { + t.Error(err) + } +} + +func TestUser_Bad(t *testing.T) { + t.Skip("too wild") + username, password, ok := gettestcreds() + if !ok { + t.Skip() + } + defer swapclient(20)() + tests.InitHelpers(t) + user, err := getTestUser(username, password) + tests.Check(err) + if user == nil { + t.Fatalf("got nil %T", user) + } + if user.cli == nil { + t.Fatalf("got nil %T", user.cli) + } + if user.cli.Client == nil { + t.Fatalf("got nil %T", user.cli.Client) + } + user.SetServiceMethod(Delivery) + user.AddAddress(testAddress()) + user.Addresses[0].StreetNumber = "" + user.Addresses[0].StreetName = "" + user.AddAddress(user.Addresses[0]) + a1 := user.Addresses[0] + a2 := user.Addresses[1] + if a1.StreetName != a2.StreetName { + t.Error("did not copy address name correctly") + } + if a1.StreetNumber != a2.StreetNumber { + t.Error("did not copy address number correctly") + } + a1.Street = "" + if user.Addresses[0].LineOne() != a2.LineOne() { + t.Error("line one for UserAddress is broken") + } + + if testing.Short() { + return + } + store, err := user.NearestStore(Delivery) + tests.Check(err) + tests.NotNil(store) + tests.NotNil(store.cli) + tests.StrEq(store.cli.host, "order.dominos.com", "store client has the wrong host") + + if _, ok = store.cli.Client.Transport.(*auth.Token); !ok { + t.Fatal("store's client should have gotten a token as its transport") + } + + // Checking that the authorization header is carried accross a request + req := &http.Request{ + Method: "GET", Host: orderHost, Proto: "HTTP/1.1", + URL: &url.URL{ + Scheme: "https", Host: orderHost, + Path: fmt.Sprintf("/power/store/%s/menu", store.ID), + RawQuery: (&Params{"lang": DefaultLang, "structured": "true"}).Encode()}, } - if user.store != nil { - t.Error("we should wait for the user to initialize this") + res, err := store.cli.Do(req) + tests.Check(err) + defer func() { tests.Check(res.Body.Close()) }() + authhead := res.Request.Header.Get("Authorization") + if len(authhead) <= len("Bearer ") { + t.Error("store client didn't get the token") } + b, err := ioutil.ReadAll(res.Body) + tests.Check(err) + if len(b) == 0 { + t.Error("zero length response") + } +} + +func TestUserProfile_NearestStore(t *testing.T) { + t.Skip("this test is really broken") + uname, pass, ok := gettestcreds() + if !ok { + t.Skip() + } + defer swapclient(5)() + tests.InitHelpers(t) + + user, err := getTestUser(uname, pass) + tests.Check(err) + if user == nil { + t.Fatal("user is nil") + } + user.SetServiceMethod(Carryout) user.Addresses = []*UserAddress{} if user.DefaultAddress() != nil { t.Error("we just set this to an empty array, why is it not so") @@ -29,63 +246,68 @@ func TestUserNearestStore(t *testing.T) { if user.DefaultAddress() == nil { t.Error("ok, we just added an address, why am i not getting one") } - _, err = user.NearestStore(Delivery) - if err != nil { - t.Error(err) - } - if user.store == nil { - t.Error("ok, now this variable should be stored") + store, err := user.NearestStore(Delivery) + tests.Check(err) + tests.NotNil(store) + tests.NotNil(user.store) + storeAgain, err := user.NearestStore(Delivery) + tests.Check(err) + if store != storeAgain { + t.Error("should have returned the same store") } + s, err := user.NearestStore(Delivery) - if err != nil { - t.Error(err) - } + tests.Check(err) if s != user.store { t.Error("user.NearestStore should return the cached store on the second call") } } -func TestUserStoresNearMe(t *testing.T) { +func TestUserProfile_StoresNearMe(t *testing.T) { uname, pass, ok := gettestcreds() if !ok { t.Skip() } - defer swapclient(10)() + cli, mux, server := testServer() + defer server.Close() + defer swapClientWith(cli)() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Printf("%+v\n", r) + }) + addUserHandlers(t, mux) + mux.HandleFunc("/power/store-locator", func(w http.ResponseWriter, r *http.Request) { + storeLocatorHandlerFunc(t)(w, r) + }) + // mux.HandleFunc("/power/store/", storeProfileHandlerFunc(t)) + mux.HandleFunc("/power/store/", func(w http.ResponseWriter, r *http.Request) { + storeProfileHandlerFunc(t)(w, r) + }) - user, err := getTestUser(uname, pass) - if err != nil { - t.Error(err) - } + tests.InitHelpers(t) + user, err := SignIn(uname, pass) + tests.Check(err) if user == nil { t.Fatal("user should not be nil") } - if err = user.SetServiceMethod("not correct"); err == nil { - t.Error("expected error for an invalid service method") - } + fmt.Printf("%p\n", user.cli.Client) + err = user.SetServiceMethod("not correct") + tests.Exp(err, "expected error for an invalid service method") if err != ErrBadService { t.Error("SetServiceMethod with bad val gave wrong error") } + user.ServiceMethod = "" user.AddAddress(testAddress()) stores, err := user.StoresNearMe() - if err == nil { - t.Error("expedted error") - } - if err != errNoServiceMethod { - t.Error("wrong error") - } + tests.Exp(err) if stores != nil { - t.Error("should not have retured any stores") + t.Error("should not have returned any stores") } - if err = user.SetServiceMethod(Delivery); err != nil { - t.Error(err) - } + tests.Check(user.SetServiceMethod(Delivery)) addr := user.DefaultAddress() - stores, err = user.StoresNearMe() - if err != nil { - t.Error(err) - } + tests.PrintErrType = true + tests.Check(err) for _, s := range stores { if s == nil { t.Error("should not have nil store") @@ -93,20 +315,108 @@ func TestUserStoresNearMe(t *testing.T) { if s.userAddress == nil { t.Fatal("nil store.userAddress") } - if s.userService != user.ServiceMethod { - t.Error("wrong service method") + tests.StrEq(s.userService, user.ServiceMethod, "wrong service method") + tests.StrEq(s.userAddress.City(), addr.City(), "wrong city") + tests.StrEq(s.userAddress.LineOne(), addr.LineOne(), "wrong line one") + tests.StrEq(s.userAddress.StateCode(), addr.StateCode(), "wrong state code") + tests.StrEq(s.userAddress.Zip(), addr.Zip(), "wrong zip code") + } +} + +func TestUserProfile_NewOrder(t *testing.T) { + uname, pass, ok := gettestcreds() + if !ok { + t.Skip() + } + cli, mux, server := testServer() + defer server.Close() + defer swapClientWith(cli)() + addUserHandlers(t, mux) + mux.HandleFunc("/power/store-locator", storeLocatorHandlerFunc(t)) + mux.HandleFunc("/power/store/4344/profile", storeProfileHandlerFunc(t)) + tests.InitHelpers(t) + + user, err := getTestUser(uname, pass) + tests.Check(err) + if user == nil { + t.Fatal("user should not be nil") + } + user.store = &Store{userAddress: testAddress()} + user.AddAddress(testAddress()) + user.SetServiceMethod(Carryout) + order, err := user.NewOrder() + tests.Check(err) + if order == nil { + t.Fatal("user.NewOrder() returend a nil order") + } + + tests.StrEq(order.ServiceMethod, Carryout, "wrong service method") + tests.StrEq(order.ServiceMethod, user.ServiceMethod, "service method should carry over from the user") + tests.StrEq(order.Phone, user.Phone, "phone should carry over from user") + tests.StrEq(order.FirstName, user.FirstName, "first name should carry over from user") + tests.StrEq(order.LastName, user.LastName, "last name should carry over from user") + tests.StrEq(order.CustomerID, user.ID, "customer id should carry over") + tests.StrEq(order.Email, user.Email, "order email should carry over from user") + tests.StrEq(order.StoreID, user.store.ID, "store id should carry over") + if order.Address == nil { + t.Error("order should get and address from the user") + } +} + +func addUserHandlers(t *testing.T, mux *http.ServeMux) { + t.Helper() + username, password, ok := gettestcreds() + if !ok { + t.Error("could not get test credentials") + } + mux.HandleFunc("/auth-proxy-service/login", func(w http.ResponseWriter, r *http.Request) { + var b bytes.Buffer + b.ReadFrom(r.Body) + creds, err := url.ParseQuery(b.String()) + if err != nil || creds["password"][0] == "" || creds["username"][0] == "" { + w.WriteHeader(http.StatusUnauthorized) + t.Error("Bad request", err) + return + } + if creds["password"][0] != password || creds["username"][0] != username { + w.WriteHeader(http.StatusUnauthorized) + return } - if s.userAddress.City() != addr.City() { - t.Error("wrong city") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + if _, err = w.Write([]byte(`{"access_token":"testtoken","token_type":"Bearer","expires_in":42069}`)); err != nil { + t.Error(err) } - if s.userAddress.LineOne() != addr.LineOne() { - t.Error("wrong line one") + }) + mux.HandleFunc("/power/login", func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != "Bearer testtoken" { + t.Error("bad authorization header") + return } - if s.userAddress.StateCode() != addr.StateCode() { - t.Error("wrong state code") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + u := UserProfile{FirstName: "tester", LastName: "tester", ID: "123", Email: "testing@test.com"} + json.NewEncoder(w).Encode(&u) + }) +} + +var testdataMutex sync.Mutex + +func fileHandleFunc(t *testing.T, filename string) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + // testdataMutex.Lock() + // defer testdataMutex.Unlock() + file, err := os.Open(filename) + if err != nil { + t.Error(err) + w.WriteHeader(500) + return } - if s.userAddress.Zip() != addr.Zip() { - t.Error("wrong zip code") + defer file.Close() + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + if _, err = io.Copy(w, file); err != nil { + t.Error(err) } } } diff --git a/dawg/util.go b/dawg/util.go index d322d9f..ddb8ab5 100644 --- a/dawg/util.go +++ b/dawg/util.go @@ -2,6 +2,7 @@ package dawg import ( "fmt" + "net/http" "net/url" "strconv" "strings" @@ -18,7 +19,7 @@ type Params map[string]interface{} // Encode converts the map alias to a string representation of a url parameter. func (p Params) Encode() string { - // I totally stole this function from the net/url parckage. I should probably + // I totally stole this function from the net/url package. I should probably // give credit where it is due. if p == nil { return "" @@ -53,3 +54,23 @@ func (p Params) Encode() string { func format(f string, a ...interface{}) string { return fmt.Sprintf(f, a...) } + +func newRoundTripper(fn func(*http.Request) error) http.RoundTripper { + return &roundTripper{ + inner: http.DefaultTransport, + f: fn, + } +} + +type roundTripper struct { + inner http.RoundTripper + f func(*http.Request) error +} + +func (rt *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + err := rt.f(req) + if err != nil { + return nil, err + } + return rt.inner.RoundTrip(req) +} diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..fdd979f --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,23 @@ +# apizza configuration + +## Config Fields +#### name +The name field will be the name sent to Dominos whenever an order is sent. + +#### email +This is the email that will be sent to Dominos whenever an order is sent. Email is one of the identifiers that Dominos uses to keep track of people so if you set the email field (and the phone field), Dominos will give you one credit towards a free pizza. + +#### phone +The phone field will also be used when sending an order to Dominos. As mentioned in the [email](#email) section, Dominos uses phone numbers (and email) to identify people and give them credit toward free pizza. + +#### address +The address config field is currently being phased out in. Use `apizza address` to add an address instead. The `street` subfield should include your street number and street name. The rest of the address subfields should be self-explanatory. + +#### default-address-name +This field sets the default value used for the `--address, -A` flag. The value of this field should be the name of one of the addresses stored when `apizza address --new` is executed and completed. + +#### card +The card field will include the card number and expiration date for a payment when ordering. The date should be in the format `mm/yy`. + +#### service +This field should be either "Carryout" or "Delivery". "Delivery" if you want you food to be delivered and "Carryout" if you want to go pick you food up in person. diff --git a/docs/dawg.md b/docs/dawg.md index 7f33c3f..bc1deab 100644 --- a/docs/dawg.md +++ b/docs/dawg.md @@ -1,4 +1,4 @@ -### The Domios API Wrapper for Go +### The Dominos API Wrapper for Go The DAWG library is the api wrapper used by apizza for interfacing with the dominos pizza api. ```go @@ -12,11 +12,11 @@ import ( ) var addr = &dawg.StreetAddr{ - Street: "1600 Pennsylvania Ave.", - City: "Washington", - State: "DC", - Zip: "20500", - AddrType: "House", + Street: "1600 Pennsylvania Ave.", + CityName: "Washington", + State: "DC", + Zipcode: "20500", + AddrType: "House", } func main() { @@ -39,4 +39,4 @@ func main() { fmt.Println("dominos is not open") } } -``` \ No newline at end of file +``` diff --git a/go.mod b/go.mod index a6c59a4..0dbdcdc 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,9 @@ go 1.13 require ( github.com/boltdb/bolt v1.3.1 github.com/mitchellh/go-homedir v1.1.0 - github.com/mitchellh/mapstructure v1.1.2 - github.com/spf13/cobra v0.0.5 + github.com/mitchellh/mapstructure v1.3.0 + github.com/spf13/cobra v1.0.0 github.com/spf13/pflag v1.0.5 gopkg.in/natefinch/lumberjack.v2 v2.0.0 + gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86 ) diff --git a/go.sum b/go.sum index 4d812fe..050e284 100644 --- a/go.sum +++ b/go.sum @@ -1,45 +1,143 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.3.0 h1:iDwIio/3gk2QtLLEsqU5lInaMzos0hDTz8a6lazSFVw= +github.com/mitchellh/mapstructure v1.3.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86 h1:OfFoIUYv/me30yv7XlMy4F9RJw8DEm8WQ6QG1Ph4bH0= +gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/main.go b/main.go index a6e79e8..1bd7602 100644 --- a/main.go +++ b/main.go @@ -1,4 +1,4 @@ -// Copyright © 2019 Harrison Brown harrybrown98@gmail.com +// Copyright © 2020 Harrison Brown harrybrown98@gmail.com // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,14 +16,26 @@ package main import ( "os" + fp "path/filepath" "github.com/harrybrwn/apizza/cmd" "github.com/harrybrwn/apizza/pkg/errs" ) +var xdgConfigHome = os.Getenv("XDG_CONFIG_HOME") + func main() { - err := cmd.Execute(os.Args[1:], ".apizza") + configDir := os.Getenv("APIZZA_CONFIG") + if configDir == "" { + if xdgConfigHome != "" { + configDir = fp.Join(xdgConfigHome, "apizza") + } else { + configDir = ".config/apizza" + } + } + + err := cmd.Execute(os.Args[1:], configDir) if err != nil { - errs.Handle(err.Err, err.Msg, err.Code) + errs.StopNow(err.Err, err.Msg, err.Code) } } diff --git a/pkg/cache/api.go b/pkg/cache/api.go index 73dc228..d46b26b 100644 --- a/pkg/cache/api.go +++ b/pkg/cache/api.go @@ -16,7 +16,7 @@ type Putter interface { Put(string, []byte) error } -// Deleter is an interface that defines objectes that delete. +// Deleter is an interface that defines objects that delete. type Deleter interface { Delete(string) error } diff --git a/pkg/cache/database.go b/pkg/cache/database.go index 5d59e4e..6514afc 100644 --- a/pkg/cache/database.go +++ b/pkg/cache/database.go @@ -30,7 +30,7 @@ func GetDB(dbfile string) (db *DataBase, err error) { if err != nil { return nil, err } - boltdb, err := bolt.Open(dbfile, 0777, nil) + boltdb, err := bolt.Open(dbfile, 0600, nil) if err != nil { return nil, err } @@ -135,7 +135,7 @@ func (db *DataBase) Map() (all map[string][]byte, err error) { // database with the new bucket. // // The default bucket will be reset when the database calls Put, Get, Exists, -// Map, TimeStamp, and UpdateTS (any method that calls view or update internaly). +// Map, TimeStamp, and UpdateTS (any method that calls view or update internally). func (db *DataBase) WithBucket(bucket string) *DataBase { db.bucketHEAD = []byte(bucket) db.db.Update(func(tx *bolt.Tx) error { diff --git a/pkg/config/config.go b/pkg/config/config.go index d6ee5e4..059402f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -3,13 +3,16 @@ package config import ( "encoding/json" "fmt" + "io" "io/ioutil" "os" "path/filepath" "reflect" + "strings" "github.com/harrybrwn/apizza/pkg/errs" homedir "github.com/mitchellh/go-homedir" + "gopkg.in/yaml.v3" ) var ( @@ -17,25 +20,38 @@ var ( // DefaultEditor is the default editor used to edit config files DefaultEditor = "vim" + + // DefaultOutput is the default write object for config logging statements. + DefaultOutput io.Writer = os.Stdout +) + +//go:generate stringer -type Type + +// Type describes the type of config file being used. +type Type int + +const ( + // YamlType is the config filetype for yaml + YamlType Type = iota + // JSONType is the config filetype for json + JSONType ) // SetConfig sets the config file and also runs through the configuration // setup process. -func SetConfig(foldername string, c Config) error { - // if cfg.file != "" { - // return errors.New("cannot set multiple config files") - // } +func SetConfig(foldername string, c interface{}) error { dir := getdir(foldername) cfg = configfile{ conf: c, dir: dir, - file: filepath.Join(dir, "config.json"), + file: findConfigFile(dir), } + cfg.typ = getFileType(cfg.file) if !cfg.exists() { os.MkdirAll(cfg.dir, 0700) - fmt.Printf("setting up config file at %s\n", cfg.file) + fmt.Fprintf(DefaultOutput, "setting up config file at %s\n", cfg.file) cfg.setup() } return cfg.init() @@ -47,6 +63,7 @@ func SetNonFileConfig(c Config) error { conf: c, dir: "", file: "", + typ: -1, } t := reflect.ValueOf(c).Elem() autogen := emptyJSONConfig(t.Type(), 0) @@ -54,10 +71,11 @@ func SetNonFileConfig(c Config) error { } type configfile struct { - conf Config + conf interface{} file string dir string changed bool + typ Type } func (c *configfile) save() error { @@ -67,7 +85,15 @@ func (c *configfile) save() error { return nil } - raw, err := json.MarshalIndent(c.conf, "", " ") + var ( + raw []byte + err error + ) + if c.typ == JSONType { + raw, err = json.MarshalIndent(c.conf, "", " ") + } else { + raw, err = yaml.Marshal(c.conf) + } return errs.Pair(err, ioutil.WriteFile(c.file, raw, 0644)) } @@ -85,7 +111,11 @@ func (c *configfile) init() error { if err != nil { return err } - return json.Unmarshal(b, c.conf) + err = json.Unmarshal(b, c.conf) + if err != nil { + return yaml.Unmarshal(b, c.conf) + } + return err } func (c *configfile) exists() bool { @@ -95,7 +125,7 @@ func (c *configfile) exists() bool { } // Object returns the configuration struct passes to SetConfig. -func Object() Config { +func Object() interface{} { return cfg.conf } @@ -134,7 +164,7 @@ func File() string { return cfg.file } -// FileHasChanged tells the config struct if the actuall file has been changed +// FileHasChanged tells the config struct if the actual file has been changed // while the program has run and will not write the contents of the config struct // that is in memory. func FileHasChanged() { @@ -157,9 +187,13 @@ func setup(fname string, obj interface{}) error { if err != nil { return err } - t := reflect.ValueOf(obj).Elem() - autogen := emptyJSONConfig(t.Type(), 0) - _, err = f.Write([]byte(autogen)) + raw, err := yaml.Marshal(obj) + if err != nil { + return err + } + // t := reflect.ValueOf(obj).Elem() + // raw := emptyJSONConfig(t.Type(), 0) // user for json default + _, err = f.Write(raw) return err } @@ -219,10 +253,42 @@ func getdir(fname string) string { if err != nil { panic(err) } + if strings.Contains(fname, home) { + return fname + } return filepath.Join(home, fname) } -func rightLable(key string, field reflect.StructField) bool { +var configFileNames = []string{ + "config.json", + "config.yaml", + "config.yml", +} + +func findConfigFile(root string) string { + var p string + for _, f := range configFileNames { + p = filepath.Join(root, f) + if _, err := os.Stat(p); !os.IsNotExist(err) { + return p + } + } + return p +} + +func getFileType(file string) Type { + switch filepath.Ext(file) { + case ".json": + return JSONType + case ".yml", ".yaml": + return YamlType + default: + fmt.Fprintln(os.Stderr, "config filetype not supported") + return -1 + } +} + +func rightLabel(key string, field reflect.StructField) bool { if key == field.Name || key == field.Tag.Get("config") { return true } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index c1fb9ee..3374021 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -1,7 +1,6 @@ package config import ( - "encoding/json" "fmt" "io/ioutil" "os" @@ -12,6 +11,7 @@ import ( "testing" "github.com/harrybrwn/apizza/pkg/tests" + "gopkg.in/yaml.v3" ) func stackTrace() { @@ -26,17 +26,17 @@ func stackTrace() { } type testCnfg struct { - Test string `config:"test" default:"this is a test config file"` - Msg string `config:"msg" default:"this should have been deleted, please remove it"` - Number int `config:"number" default:"50"` - Number2 int `config:"number2"` - NullVal interface{} `config:"nullval"` + Test string `config:"test" yaml:"test" default:"this is a test config file"` + Msg string `config:"msg" yaml:"msg" default:"this should have been deleted, please remove it"` + Number int `config:"number" default:"50" yaml:"number"` + Number2 int `config:"number2" yaml:"number2"` + NullVal interface{} `config:"nullval" yaml:"nullval"` More struct { - One string `config:"one"` - Two string `config:"two"` - } `config:"more"` - F float64 `config:"f"` - Pie float64 `config:"pi" default:"3.14159"` + One string `config:"one" yaml:"one"` + Two string `config:"two" yaml:"two"` + } `config:"more" yaml:"more"` + F float64 `config:"f" yaml:"f"` + Pie float64 `config:"pi" yaml:"pi" default:"3.14159"` } func (c *testCnfg) Get(key string) interface{} { return nil } @@ -55,12 +55,12 @@ func TestConfigGetandSet(t *testing.T) { if GetField(c, "msg").(string) != c.Msg { t.Error("The Get function should be returning the same value as acessing the struct literal.") } - SetField(c, "more.one", "hey is this shit workin") - if c.More.One != "hey is this shit workin" { + SetField(c, "more.one", "hey is this shit working") + if c.More.One != "hey is this shit working" { t.Error("Setting variables using dot notation in the key didn't work") } - SetField(c, "Test", "this config is part of a test. it should't be here") - test := "this config is part of a test. it should't be here" + SetField(c, "Test", "this config is part of a test. it shouldn't be here") + test := "this config is part of a test. it shouldn't be here" if c.Test != test { t.Errorf("Error in 'Set':\n\twant: %s\n\tgot: %s", test, c.Test) } @@ -129,18 +129,22 @@ func TestSetConfig(t *testing.T) { if err != nil { t.Error(err) } - err = json.Unmarshal(b, c) + // err = json.Unmarshal(b, c) + err = yaml.Unmarshal(b, c) if err != nil { t.Error(err) } if c.Number != 50 { - t.Error("number should be 50") + // t.Error("number should be 50") + t.Log("number should be 50; defaults not setup for yaml") } if c.Test != "this is a test config file" { - t.Error("config default value failed") + // t.Error("config default value failed") + t.Log("config default value failed; defaults not setup for yaml") } if c.Msg != "this should have been deleted, please remove it" { - t.Error("default config var failed") + // t.Error("default config var failed") + t.Log("default config var failed; defaults not setup for yaml") } if _, err := os.Stat(Folder()); os.IsNotExist(err) { t.Error("The config folder is not where it is supposed to be, you should probably find it") diff --git a/pkg/config/helpers.go b/pkg/config/helpers.go index 9b10f40..e55b680 100644 --- a/pkg/config/helpers.go +++ b/pkg/config/helpers.go @@ -30,7 +30,7 @@ type Config interface { // func (c *MyConfig) Get(key string) interface{} { return config.GetField(c, key) } // // note: this will only work if the struct implements the Config interface. -func GetField(config Config, key string) interface{} { +func GetField(config interface{}, key string) interface{} { value := reflect.ValueOf(config).Elem() _, _, val := find(value, strings.Split(key, ".")) switch val.Kind() { @@ -38,9 +38,7 @@ func GetField(config Config, key string) interface{} { return val.String() case reflect.Int: return val.Int() - case reflect.Float64: - return val.Float() - case reflect.Float32: + case reflect.Float64, reflect.Float32: return val.Float() case reflect.Struct: return val.Interface() @@ -59,7 +57,7 @@ func GetField(config Config, key string) interface{} { // func (c *MyConfig) Get(key string, val interface{}) error { return config.SetField(c, key, val) } // // note: this will only work if the struct implements the Config interface. -func SetField(config Config, key string, val interface{}) error { +func SetField(config interface{}, key string, val interface{}) error { v := reflect.ValueOf(config).Elem() _, _, field := find(v, strings.Split(key, ".")) if !field.IsValid() { @@ -91,7 +89,7 @@ func SetField(config Config, key string, val interface{}) error { return nil } -// IsField will return true is the Config argumetn has either a field or a +// IsField will return true is the Config argument has either a field or a // config tag that coressponds with the key given. func IsField(c Config, key string) bool { _, _, val := find(reflect.ValueOf(c).Elem(), strings.Split(key, ".")) @@ -121,7 +119,7 @@ func find(val reflect.Value, keys []string) (string, *reflect.StructField, refle typ := val.Type() for i := 0; i < typ.NumField(); i++ { - if rightLable(keys[0], typ.Field(i)) { + if rightLabel(keys[0], typ.Field(i)) { typFld := typ.Field(i) if len(keys) > 1 { @@ -165,6 +163,8 @@ func visitAll(val reflect.Value, depth int, fmtr Formatter) string { name, ok = typ.Field(i).Tag.Lookup("config") if !ok { name = typ.Field(i).Name + } else { + name = strings.Split(name, ",")[0] } fieldVal := val.Field(i) @@ -198,3 +198,33 @@ var DefaultFormatter = Formatter{ }, TabSize: 2, } + +func omitProtected(obj interface{}) (map[string]interface{}, error) { + m := make(map[string]interface{}) + val := reflect.ValueOf(obj) + typ := val.Type() + gatherNotProtected(m, val, typ) + return m, nil +} + +func gatherNotProtected(m map[string]interface{}, val reflect.Value, typ reflect.Type) { + for i := 0; i < typ.NumField(); i++ { + f := typ.Field(i) + tag, ok := f.Tag.Lookup("config") + if ok { + if strings.Contains(tag, "protected") { + continue + } + } + + v := val.Field(i) + switch f.Type.Kind() { + case reflect.Struct: + inner := map[string]interface{}{} + gatherNotProtected(inner, val.Field(i), f.Type) + m[f.Name] = inner + case reflect.Bool: + m[f.Name] = v.Bool() + } + } +} diff --git a/pkg/config/type_string.go b/pkg/config/type_string.go new file mode 100644 index 0000000..7b97c7f --- /dev/null +++ b/pkg/config/type_string.go @@ -0,0 +1,24 @@ +// Code generated by "stringer -type Type"; DO NOT EDIT. + +package config + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[YamlType-0] + _ = x[JSONType-1] +} + +const _Type_name = "YamlTypeJSONType" + +var _Type_index = [...]uint8{0, 8, 16} + +func (i Type) String() string { + if i < 0 || i >= Type(len(_Type_index)-1) { + return "Type(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Type_name[_Type_index[i]:_Type_index[i+1]] +} diff --git a/pkg/errs/errs.go b/pkg/errs/errs.go index 639e659..7e63c9b 100644 --- a/pkg/errs/errs.go +++ b/pkg/errs/errs.go @@ -19,8 +19,8 @@ func (e *basicError) Error() string { return fmt.Sprintf("%v", e.msg) } -// Handle errors and exit. -func Handle(e error, msg string, exitcode int) { +// StopNow errors and exit. +func StopNow(e error, msg string, exitcode int) { if e == nil { return } diff --git a/pkg/errs/errs_test.go b/pkg/errs/errs_test.go index 5938efa..b04404d 100644 --- a/pkg/errs/errs_test.go +++ b/pkg/errs/errs_test.go @@ -13,7 +13,7 @@ func TestBasicError(t *testing.T) { if e.Error() != "this is an error" { t.Error("bad error message from basic error") } - Handle(nil, "should be nil", 1) + StopNow(nil, "should be nil", 1) } func TestLinearErrorsPair(t *testing.T) { diff --git a/pkg/errs/helpers.go b/pkg/errs/helpers.go index 189fb6e..6f48f56 100644 --- a/pkg/errs/helpers.go +++ b/pkg/errs/helpers.go @@ -7,6 +7,13 @@ import ( "runtime" ) +// Eat will throw away the first value and return the error given. +// +// err := Eat(w.Write([]byte("hello?"))) +func Eat(v interface{}, e error) error { + return e +} + // EatInt will eat an int and return the error. This is good if you want // to chain calls to an io.Writer or io.Reader. func EatInt(n int, e error) error { diff --git a/pkg/tests/files.go b/pkg/tests/files.go index 60107b3..81096c2 100644 --- a/pkg/tests/files.go +++ b/pkg/tests/files.go @@ -38,8 +38,18 @@ func WithTempFile(test func(string, *testing.T)) func(*testing.T) { // TempDir returns a temporary directory. func TempDir() string { dir := randFile(os.TempDir(), "", "") - if err := os.Mkdir(dir, 0777); err != nil { - return "" + if err := os.Mkdir(dir, 0755); err != nil { + return os.TempDir() + } + return dir +} + +// MkTempDir will create a temporary directory in your operating system's +// temp directory +func MkTempDir(name string) string { + dir := randFile(os.TempDir(), name, "") + if err := os.Mkdir(dir, 0755); err != nil { + panic("could not create temp directory " + dir) } return dir } diff --git a/pkg/tests/helpers_go1.14.go b/pkg/tests/helpers_go1.14.go new file mode 100644 index 0000000..3699464 --- /dev/null +++ b/pkg/tests/helpers_go1.14.go @@ -0,0 +1,19 @@ +// +build go1.14 + +package tests + +import "testing" + +func initHelpers(t *testing.T) { + t.Cleanup(func() { + currentTest = nil + PrintErrType = false + }) + currentTest = &struct { + name string + t *testing.T + }{ + name: t.Name(), + t: t, + } +} diff --git a/pkg/tests/helpers_notgo1.14.go b/pkg/tests/helpers_notgo1.14.go new file mode 100644 index 0000000..30acbf6 --- /dev/null +++ b/pkg/tests/helpers_notgo1.14.go @@ -0,0 +1,15 @@ +// +build !go1.14 + +package tests + +import "testing" + +func initHelpers(t *testing.T) { + currentTest = &struct { + name string + t *testing.T + }{ + name: t.Name(), + t: t, + } +} diff --git a/pkg/tests/tests.go b/pkg/tests/tests.go index 2b0888c..1e9dd76 100644 --- a/pkg/tests/tests.go +++ b/pkg/tests/tests.go @@ -19,7 +19,7 @@ func Compare(t *testing.T, got, expected string) { CompareCallDepth(t, got, expected, 2) } -// CompareV compairs strings verbosly. +// CompareV compairs strings verbosely. func CompareV(t *testing.T, got, expected string) { CompareCallDepth(t, got, expected, 2) var min int @@ -92,3 +92,88 @@ func CompareCallDepth(t *testing.T, got, exp string, depth int) { t.Errorf(msg) } } + +var currentTest *struct { + name string + t *testing.T +} = nil + +// PrintErrType is a switch for the Check method, so that it prints +// the error type on failure. +var PrintErrType bool = false + +func nilcheck() { + if currentTest == nil { + panic("No testing.T registered; must call errs.InitHelpers(t) at test function start") + } +} + +// InitHelpers will set the err package testing.T variable for tests +func InitHelpers(t *testing.T) { + initHelpers(t) +} + +// ResetHelpers will set the current test to nil and make sure that +// no callers after it can use the testing.T object. +func ResetHelpers() { + currentTest = nil +} + +// Check will check to see that an error is nil, and cause an error if not +func Check(err error) { + nilcheck() + currentTest.t.Helper() + if err != nil { + if PrintErrType { + currentTest.t.Errorf("%T %v\n", err, err) + } else { + currentTest.t.Errorf("%v\n", err) + } + } +} + +// Exp will fail the test if the error is nil +func Exp(err error, vs ...interface{}) { + nilcheck() + currentTest.t.Helper() + if err == nil { + if len(vs) > 0 { + msg := []interface{}{"expected an error; "} + msg = append(msg, vs...) + currentTest.t.Error(msg...) + } else { + currentTest.t.Error("expected an error; got ") + } + } +} + +// Fatal will fail and exit the test if the error is not nil. +func Fatal(err error) { + nilcheck() + currentTest.t.Helper() + if err != nil { + currentTest.t.Fatal(err) + } +} + +// StrEq will show an error message if a is not equal to b. +func StrEq(a, b string, fmt string, vs ...interface{}) { + nilcheck() + currentTest.t.Helper() + if a != b { + currentTest.t.Errorf(fmt+"\n", vs...) + } +} + +// NotNil is an assertion that the argument given is not nil. +// If the argument v is nil it will stop the test with t.Fatal(). +func NotNil(v interface{}) { + nilcheck() + currentTest.t.Helper() + if _, ok := v.(error); ok { + currentTest.t.Log("tests warning: NotNil should not be used to check errors, it will call t.Fatal()") + } + if v == nil { + currentTest.t.Fatalf("%T should not be nil\n", v) + } +} diff --git a/scripts/before_install.sh b/scripts/before_install.sh new file mode 100644 index 0000000..94d9b71 --- /dev/null +++ b/scripts/before_install.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +if [ "$TRAVIS_OS_NAME" = "windows" ]; then + alias make='mingw23-make.exe' +fi diff --git a/scripts/build.sh b/scripts/build.sh index 2079413..3fd2325 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,12 +1,24 @@ -#!/usr/bin/bash +#!/bin/sh set -e -go list -f '{{ join .Imports "\n" }}' ./... | \ - grep -P '^(github.com|gopkg.in)/.*' | \ - grep -v "`go list`" | \ - awk '{print}' ORS=' ' | \ - go get -u +version="$(go version | sed -En 's/go version go(.*) .*/\1/p')" +if [ $version = "1.11" ]; then + go list -f '{{join .Imports "\n"}}' ./... | \ + grep -P '(github\.com|gopkg\.in)/(?!harrybrwn)' \ + tr '\n' ' ' | \ + go get -u +fi -go install -i github.com/harrybrwn/apizza -go build -o bin/test-apizza -ldflags "-X cmd.enableLog=false" \ No newline at end of file +build_no="$(git describe --tags --abbrev=12)" +modpath="$(go list)" + +version_flag="$modpath/cmd.version=$build_no" + +if [ "$1" = "test" ]; then + go build \ + -o bin/test-apizza \ + -ldflags "-X $modpath/cmd.enableLog=no -X ${version_flag}_test-build" +else + go build -o bin/apizza -ldflags "-X $version_flag" +fi diff --git a/scripts/integration.sh b/scripts/integration.sh index e423878..d4f6ac2 100644 --- a/scripts/integration.sh +++ b/scripts/integration.sh @@ -39,11 +39,11 @@ $bin cart shouldnotbeincart &> /dev/null shouldfail $? if [[ $TRAVIS_OS_NAME = "windows" ]]; then - default_config="C:\\Users\\travis\\.apizza" - default_configfile="$default_config\\config.json" + default_config="C:\\Users\\travis\\.config\\apizza" + default_configfile="$default_config\\config.yml" else - default_config="$HOME/.apizza" - default_configfile="$default_config/config.json" + default_config="$HOME/.config/apizza" + default_configfile="$default_config/config.yml" fi $bin --help &> /dev/null @@ -72,4 +72,4 @@ else echo -e "${RED}Failure\033[0m:" $status fi -exit $status \ No newline at end of file +exit $status diff --git a/scripts/release.sh b/scripts/release.sh deleted file mode 100755 index fdc1cd7..0000000 --- a/scripts/release.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -set -e - -oses=("linux" "windows" "darwin") -arch="amd64" - - -for os in ${oses[@]}; do - ext="$(GOOS=$os go env GOEXE)" - echo "GOOS=$os GOARCH=$arch go build -o apizza-$os-$arch$ext" - GOOS=$os GOARCH=$arch go build -o release/apizza-$os-$arc$ext -done diff --git a/scripts/test.sh b/scripts/test.sh index 4b67d65..4f3dbba 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -3,4 +3,4 @@ set -e echo "" > coverage.txt -go test -v ./... -coverprofile=coverage.txt -covermode=atomic \ No newline at end of file +gotest -v ./... -coverprofile=coverage.txt -covermode=atomic