Merge branch 'feature/usps-shipping' into develop

This commit is contained in:
Nathan Chapman 2022-04-24 12:38:48 -06:00
commit 1e49f36f7a
21 changed files with 254 additions and 32 deletions

View File

@ -71,3 +71,27 @@ class ShippingMethodType:
(PRICE_BASED, "Price based shipping"), (PRICE_BASED, "Price based shipping"),
(WEIGHT_BASED, "Weight based shipping"), (WEIGHT_BASED, "Weight based shipping"),
] ]
class ShippingService:
FIRST_CLASS = "FIRST CLASS"
PRIORITY = "PRIORITY"
PRIORITY_COMMERCIAL = "PRIORITY COMMERCIAL"
CHOICES = [
(FIRST_CLASS, "First Class"),
(PRIORITY, "Priority"),
(PRIORITY_COMMERCIAL, "Priority Commercial")
]
class ShippingContainer:
LG_FLAT_RATE_BOX = "LG FLAT RATE BOX"
REGIONAL_RATE_BOX_A = "REGIONALRATEBOXA"
REGIONAL_RATE_BOX_B = "REGIONALRATEBOXB"
VARIABLE = "VARIABLE"
CHOICES = [
(LG_FLAT_RATE_BOX, "Flate Rate Box - Large"),
(REGIONAL_RATE_BOX_A, "Regional Rate Box A"),
(REGIONAL_RATE_BOX_B, "Regional Rate Box B"),
(VARIABLE, "Variable")
]

View File

@ -0,0 +1,22 @@
# Generated by Django 4.0.2 on 2022-04-24 16:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0005_alter_product_options_product_sorting'),
]
operations = [
migrations.AlterModelOptions(
name='order',
options={'ordering': ('-created_at',)},
),
migrations.AddField(
model_name='order',
name='shipping_total',
field=models.DecimalField(decimal_places=2, default=0, max_digits=5),
),
]

View File

@ -9,6 +9,8 @@ from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.urls import reverse from django.urls import reverse
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.serializers.json import DjangoJSONEncoder
from django.forms.models import model_to_dict
from django_measurement.models import MeasurementField from django_measurement.models import MeasurementField
@ -25,6 +27,10 @@ from .weight import WeightUnits, zero_weight
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ProductEncoder(DjangoJSONEncoder):
def default(self, obj):
logger.info(f"\n{obj}\n")
return super().default(obj)
class ProductManager(models.Manager): class ProductManager(models.Manager):
def get_queryset(self): def get_queryset(self):
@ -69,7 +75,7 @@ class Product(models.Model):
try: try:
return self.productphoto_set.all()[1] return self.productphoto_set.all()[1]
except IndexError: except IndexError:
pass return 'No image'
class Meta: class Meta:
ordering = ['sorting', 'name'] ordering = ['sorting', 'name']
@ -200,6 +206,7 @@ class Order(models.Model):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
) )
coupon = models.ForeignKey( coupon = models.ForeignKey(
Coupon, Coupon,
related_name='orders', related_name='orders',
@ -207,6 +214,12 @@ class Order(models.Model):
null=True null=True
) )
shipping_total = models.DecimalField(
max_digits=5,
decimal_places=2,
default=0
)
total_net_amount = models.DecimalField( total_net_amount = models.DecimalField(
max_digits=10, max_digits=10,
decimal_places=2, decimal_places=2,
@ -237,7 +250,7 @@ class Order(models.Model):
return Decimal('0') return Decimal('0')
def get_total_price_after_discount(self): def get_total_price_after_discount(self):
return round(self.total_net_amount - self.get_discount(), 2) return round((self.total_net_amount - self.get_discount()) + self.shipping_total, 2)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dashboard:order-detail', kwargs={'pk': self.pk}) return reverse('dashboard:order-detail', kwargs={'pk': self.pk})

38
src/core/usps.py Normal file
View File

@ -0,0 +1,38 @@
import json
import requests
import xmltodict
from lxml import etree
from usps import USPSApi
class USPSApiWithRate(USPSApi):
urls = {
'tracking': 'TrackV2{test}&XML={xml}',
'label': 'eVS{test}&XML={xml}',
'validate': 'Verify&XML={xml}',
'rate': 'RateV4&XML={xml}',
}
def get_rate(self, *args, **kwargs):
return Rate(self, *args, **kwargs)
class Rate:
def __init__(self, usps, request, **kwargs):
xml = etree.Element('RateV4Request', {'USERID': usps.api_user_id})
etree.SubElement(xml, 'Revision').text = '2'
package = etree.SubElement(xml, 'Package', {'ID': '0'})
etree.SubElement(package, 'Service').text = request['service']
etree.SubElement(package, 'ZipOrigination').text = request['zip_origination']
etree.SubElement(package, 'ZipDestination').text = request['zip_destination']
etree.SubElement(package, 'Pounds').text = request['pounds']
etree.SubElement(package, 'Ounces').text = request['ounces']
etree.SubElement(package, 'Container').text = request['container']
etree.SubElement(package, 'Width').text = request['width']
etree.SubElement(package, 'Length').text = request['length']
etree.SubElement(package, 'Height').text = request['height']
etree.SubElement(package, 'Girth').text = request['girth']
etree.SubElement(package, 'Machinable').text = request['machinable']
self.result = usps.send_request('rate', xml)

View File

@ -103,11 +103,12 @@
</div> </div>
<div class="panel__item"> <div class="panel__item">
<p> <p>
<span>Subtotal: {{order.total_net_amount}}</span><br> <span>Subtotal: ${{order.total_net_amount}}</span><br>
{% if order.coupon %} {% if order.coupon %}
<span>Discount: {{order.coupon.discount_value}} {{order.coupon.get_discount_value_type_display}}</span><br> <span>Discount: {{order.coupon.discount_value}} {{order.coupon.get_discount_value_type_display}}</span><br>
{% endif %} {% endif %}
<span>Total: {{order.get_total_price_after_discount}}</span> <span>Shipping: ${{order.shipping_total}}</span><br>
<span>Total: ${{order.get_total_price_after_discount}}</span>
</p> </p>
</div> </div>
</section> </section>

View File

@ -22,7 +22,7 @@
<span class="order__status--display"> <span class="order__status--display">
<div class="status__dot order__status--{{order.status}}"></div> <div class="status__dot order__status--{{order.status}}"></div>
{{order.get_status_display}}</span> {{order.get_status_display}}</span>
<span>${{order.total_net_amount}}</span> <span>${{order.get_total_price_after_discount}}</span>
</a> </a>
{% empty %} {% empty %}
<span class="object__item">No orders</span> <span class="object__item">No orders</span>

View File

@ -20,6 +20,8 @@ CACHE_CONFIG = {
PAYPAL_CLIENT_ID = os.environ.get('PAYPAL_CLIENT_ID', '') PAYPAL_CLIENT_ID = os.environ.get('PAYPAL_CLIENT_ID', '')
PAYPAL_SECRET_ID = os.environ.get('PAYPAL_SECRET_ID', '') PAYPAL_SECRET_ID = os.environ.get('PAYPAL_SECRET_ID', '')
USPS_USER_ID = os.environ.get('USPS_USER_ID', '639NATHA3105')
DEFAULT_ZIP_ORIGINATION = os.environ.get('DEFAULT_ZIP_ORIGINATION', '98368')
ANYMAIL_CONFIG = { ANYMAIL_CONFIG = {
'MAILGUN_API_KEY': os.environ.get('MAILGUN_API_KEY', ''), 'MAILGUN_API_KEY': os.environ.get('MAILGUN_API_KEY', ''),

View File

@ -93,6 +93,7 @@ DATABASES = {
CACHES = {'default': CACHE_CONFIG} CACHES = {'default': CACHE_CONFIG}
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
# Password validation # Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

View File

@ -1,12 +1,14 @@
:root { :root {
--fg-color: #34201a; --fg-color: #34201a;
--fg-alt-color: #663a2d; --fg-alt-color: #663a2d;
--bg-color: #f5f5f5; --bg-color: #fffbf8;
--bg-alt-color: #c8a783; --bg-alt-color: #b07952;
--gray-color: #9d9d9d; --gray-color: #9d9d9d;
--yellow-color: #f8a911; --yellow-color: #f8a911;
--yellow-alt-color: #ffce6f; --yellow-alt-color: #ffce6f;
--yellow-dark-color: #b27606; --yellow-dark-color: #b27606;
--red-color: #d43131;
--green-color: #3ea165;
--default-border: 2px solid var(--gray-color); --default-border: 2px solid var(--gray-color);
} }
@ -48,6 +50,7 @@ h1, h2, h3, h4, h5 {
h1 { h1 {
margin-top: 0; margin-top: 0;
font-family: 'Vollkorn', serif;
font-size: 2.488rem; font-size: 2.488rem;
} }
@ -443,6 +446,53 @@ section:not(:last-child) {
flex-direction: column; flex-direction: column;
} }
/* Site Banner
========================================================================== */
.site__banner {
background-color: rgba(0, 0, 0, 0.44);
background-blend-mode: multiply;
background-image: url("/static/images/site_banner.jpg");
background-size: cover;
background-position: center;
color: white;
text-align: center;
padding: 8rem 1rem;
font-family: 'Vollkorn', serif;
}
.site__banner h1 {
font-size: 3.5rem;
}
.site__banner p {
text-transform: lowercase;
font-variant: small-caps;
font-size: 2rem;
}
/* Messages
========================================================================== */
.messages {
text-align: center;
font-weight: bold;
margin-bottom: 0 !important;
}
.messages p {
margin-bottom: 0;
}
.messages .success {
background-color: var(--green-color);
}
.messages .warning {
background-color: var(--yellow-color);
}
.messages .error {
background-color: var(--red-color);
}
/* Site Cart /* Site Cart
========================================================================== */ ========================================================================== */
.site__cart { .site__cart {

View File

@ -1,17 +1,23 @@
import logging import logging
from decimal import Decimal from decimal import Decimal
from django.conf import settings from django.conf import settings
from core.models import Product, OrderLine, Coupon from django.contrib import messages
from .payments import CreateOrder
from core.models import Product, OrderLine, Coupon
from core.usps import USPSApiWithRate
from core import ( from core import (
DiscountValueType, DiscountValueType,
VoucherType, VoucherType,
TransactionStatus, TransactionStatus,
OrderStatus, OrderStatus,
ShippingMethodType ShippingMethodType,
ShippingService,
ShippingContainer
) )
from .payments import CreateOrder
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Cart: class Cart:
@ -23,7 +29,7 @@ class Cart:
cart = self.session[settings.CART_SESSION_ID] = {} cart = self.session[settings.CART_SESSION_ID] = {}
self.cart = cart self.cart = cart
def add(self, product, quantity=1, grind='', update_quantity=False): def add(self, request, product, quantity=1, grind='', update_quantity=False):
product_id = str(product.id) product_id = str(product.id)
if product_id not in self.cart: if product_id not in self.cart:
self.cart[product_id] = { self.cart[product_id] = {
@ -36,7 +42,10 @@ class Cart:
self.cart[product_id]['quantity'] = quantity self.cart[product_id]['quantity'] = quantity
else: else:
self.cart[product_id]['quantity'] += quantity self.cart[product_id]['quantity'] += quantity
self.save() if len(self) <= 20:
self.save()
else:
messages.warning(request, "Cart is full: 20 items or less.")
def save(self): def save(self):
self.session[settings.CART_SESSION_ID] = self.cart self.session[settings.CART_SESSION_ID] = self.cart
@ -63,6 +72,26 @@ class Cart:
def __len__(self): def __len__(self):
return sum(item['quantity'] for item in self.cart.values()) return sum(item['quantity'] for item in self.cart.values())
def get_total_weight(self):
return sum([item['product'].weight.value * item['quantity'] for item in self])
def get_shipping_box(self):
logger.info(len(self))
if len(self) > 6 and len(self) <= 10:
return ShippingContainer.LG_FLAT_RATE_BOX
elif len(self) > 2 and len(self) <= 6:
return ShippingContainer.REGIONAL_RATE_BOX_B
elif len(self) <= 2:
return ShippingContainer.REGIONAL_RATE_BOX_A
else:
return ShippingContainer.VARIABLE
def get_shipping_cost(self):
usps_rate_request = self.build_usps_rate_request()
usps = USPSApiWithRate(settings.USPS_USER_ID, test=True)
validation = usps.get_rate(usps_rate_request)
return Decimal(validation.result['RateV4Response']['Package']['Postage']['CommercialRate'])
def get_total_price(self): def get_total_price(self):
return sum(Decimal(item['price']) * item['quantity'] for item in self.cart.values()) return sum(Decimal(item['price']) * item['quantity'] for item in self.cart.values())
@ -74,6 +103,22 @@ class Cart:
pass pass
self.session.modified = True self.session.modified = True
def build_usps_rate_request(self):
return \
{
'service': ShippingService.PRIORITY_COMMERCIAL,
'zip_origination': settings.DEFAULT_ZIP_ORIGINATION,
'zip_destination': f'{self.session.get("shipping_address")["postal_code"]}',
'pounds': '0',
'ounces': f'{self.get_total_weight()}',
'container': f'{self.get_shipping_box()}',
'width': '',
'length': '',
'height': '',
'girth': '',
'machinable': 'TRUE'
}
def build_order_params(self): def build_order_params(self):
return \ return \
{ {
@ -81,7 +126,7 @@ class Cart:
'total_price': f'{self.get_total_price_after_discount()}', 'total_price': f'{self.get_total_price_after_discount()}',
'item_total': f'{self.get_total_price()}', 'item_total': f'{self.get_total_price()}',
'discount': f'{self.get_discount()}', 'discount': f'{self.get_discount()}',
'shipping_price': '0', 'shipping_price': f'{self.get_shipping_cost()}',
'tax_total': '0', 'tax_total': '0',
'shipping_method': 'US POSTAL SERVICE', 'shipping_method': 'US POSTAL SERVICE',
'shipping_address': self.build_shipping_address(self.session.get('shipping_address')), 'shipping_address': self.build_shipping_address(self.session.get('shipping_address')),
@ -130,5 +175,8 @@ class Cart:
return round((self.coupon.discount_value / Decimal('100')) * self.get_total_price(), 2) return round((self.coupon.discount_value / Decimal('100')) * self.get_total_price(), 2)
return Decimal('0') return Decimal('0')
def get_total_price_after_discount(self): def get_subtotal_price_after_discount(self):
return round(self.get_total_price() - self.get_discount(), 2) return round(self.get_total_price() - self.get_discount(), 2)
def get_total_price_after_discount(self):
return round(self.get_total_price() - self.get_discount() + self.get_shipping_cost(), 2)

View File

@ -96,9 +96,11 @@ class OrderCreateForm(forms.ModelForm):
model = Order model = Order
fields = ( fields = (
'total_net_amount', 'total_net_amount',
'shipping_total',
) )
widgets = { widgets = {
'total_net_amount': forms.HiddenInput() 'total_net_amount': forms.HiddenInput(),
'shipping_total': forms.HiddenInput()
} }
class CouponApplyForm(forms.Form): class CouponApplyForm(forms.Form):

View File

@ -51,7 +51,7 @@
<table class="cart__totals"> <table class="cart__totals">
<tr> <tr>
<td>Subtotal</td> <td>Subtotal</td>
<td>${{cart.get_total_price|floatformat:"2"}}</td> <td>${{ cart.get_total_price|floatformat:"2" }}</td>
</tr> </tr>
{% if cart.coupon %} {% if cart.coupon %}
<tr> <tr>
@ -61,7 +61,7 @@
{% endif %} {% endif %}
<tr> <tr>
<th>Total</th> <th>Total</th>
<td><strong>${{cart.get_total_price_after_discount|floatformat:"2"}}</strong></td> <td><strong>${{cart.get_subtotal_price_after_discount|floatformat:"2"}}</strong></td>
</tr> </tr>
</table> </table>
</div> </div>

View File

@ -5,13 +5,9 @@
<header> <header>
<h1>Contact us</h1> <h1>Contact us</h1>
<h4>Problem with your online order or have a question?</h4> <h4>Problem with your online order or have a question?</h4>
<p> <p>Please contact us, were happy to help you.</p>
Please contact us, were happy to help you over the phone<br>
<a href="tel:+13603855856">(360) 385-5856</a> between 8:00 am and 10:00 pm Pacific Time.
</p>
</header> </header>
<section> <section>
<p>Or send us a message using the form below and we'll email you back as soon as we can.</p>
<form action="{% url 'storefront:contact' %}" method="post" class="contact-form"> <form action="{% url 'storefront:contact' %}" method="post" class="contact-form">
{% csrf_token %} {% csrf_token %}
{{form.as_p}} {{form.as_p}}

View File

@ -62,6 +62,10 @@
<td>{{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}}</td> <td>{{cart.coupon.discount_value}} {{cart.coupon.get_discount_value_type_display}}</td>
</tr> </tr>
{% endif %} {% endif %}
<tr>
<td>Shipping</td>
<td>${{ cart.get_shipping_cost }}</small></td>
</tr>
<tr> <tr>
<th>Total</th> <th>Total</th>
<td><strong>${{cart.get_total_price_after_discount|floatformat:"2"}}</strong></td> <td><strong>${{cart.get_total_price_after_discount|floatformat:"2"}}</strong></td>

View File

@ -6,6 +6,10 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="site__banner">
<h1><em>Better</em>, not <em>Bitter</em></h1>
<p>ORGANIC COFFEE, SLOW ROASTED, ITALIAN STYLE</p>
</div>
<article> <article>
<section class="product__list"> <section class="product__list">
{% for product in product_list %} {% for product in product_list %}

View File

@ -4,7 +4,7 @@
{% block content %} {% block content %}
<article> <article>
<header> <header>
<h2>Reviews</h2> <h1>Reviews</h1>
</header> </header>
<section> <section>
<figure> <figure>

View File

@ -44,6 +44,7 @@ class CartTest(TestCase):
request = response.wsgi_request request = response.wsgi_request
cart = Cart(request) cart = Cart(request)
cart.add( cart.add(
request=request,
product=self.product, product=self.product,
quantity=1, quantity=1,
update_quantity=False update_quantity=False

View File

@ -17,6 +17,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django.forms.models import model_to_dict
from paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest from paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest
from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment
@ -26,6 +27,7 @@ from accounts.utils import get_or_create_customer
from accounts.forms import AddressForm as AccountAddressForm, CustomerUpdateForm from accounts.forms import AddressForm as AccountAddressForm, CustomerUpdateForm
from core.models import Product, Order, Transaction, OrderLine, Coupon from core.models import Product, Order, Transaction, OrderLine, Coupon
from core.forms import ShippingMethodForm from core.forms import ShippingMethodForm
from core import OrderStatus
from .forms import AddToCartForm, UpdateCartItemForm, OrderCreateForm, AddressForm, CouponApplyForm, ContactForm from .forms import AddToCartForm, UpdateCartItemForm, OrderCreateForm, AddressForm, CouponApplyForm, ContactForm
from .cart import Cart from .cart import Cart
@ -61,6 +63,7 @@ class CartAddProductView(SingleObjectMixin, FormView):
form = self.get_form() form = self.get_form()
if form.is_valid(): if form.is_valid():
cart.add( cart.add(
request=request,
product=self.get_object(), product=self.get_object(),
grind=form.cleaned_data['grind'], grind=form.cleaned_data['grind'],
quantity=form.cleaned_data['quantity'] quantity=form.cleaned_data['quantity']
@ -85,6 +88,7 @@ class CartUpdateProductView(SingleObjectMixin, FormView):
form = self.get_form() form = self.get_form()
if form.is_valid(): if form.is_valid():
cart.add( cart.add(
request=request,
product=self.get_object(), product=self.get_object(),
quantity=form.cleaned_data['quantity'], quantity=form.cleaned_data['quantity'],
update_quantity=form.cleaned_data['update'] update_quantity=form.cleaned_data['update']
@ -178,7 +182,8 @@ class OrderCreateView(CreateView):
def get_initial(self): def get_initial(self):
cart = Cart(self.request) cart = Cart(self.request)
initial = { initial = {
'total_net_amount': cart.get_total_price() 'total_net_amount': cart.get_total_price(),
'shipping_total': cart.get_shipping_cost()
} }
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
@ -210,13 +215,14 @@ class OrderCreateView(CreateView):
cart = Cart(self.request) cart = Cart(self.request)
shipping_address = self.request.session.get('shipping_address') shipping_address = self.request.session.get('shipping_address')
form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, form, shipping_address) form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, form, shipping_address)
form.instance.status = OrderStatus.DRAFT
self.object = form.save() self.object = form.save()
bulk_list = cart.build_bulk_list(self.object) bulk_list = cart.build_bulk_list(self.object)
objs = OrderLine.objects.bulk_create(bulk_list) objs = OrderLine.objects.bulk_create(bulk_list)
response = cart.create_order() response = cart.create_order()
data = response.result.__dict__['_dict'] data = response.result.__dict__['_dict']
cart.clear()
self.request.session['order_id'] = self.object.pk self.request.session['order_id'] = self.object.pk
return JsonResponse(data) return JsonResponse(data)
@ -224,8 +230,12 @@ class OrderCreateView(CreateView):
def paypal_order_transaction_capture(request, transaction_id): def paypal_order_transaction_capture(request, transaction_id):
if request.method =="POST": if request.method =="POST":
data = CaptureOrder().capture_order(transaction_id) data = CaptureOrder().capture_order(transaction_id)
cart = Cart(request)
transaction = Transaction.objects.get(order__pk=request.session.get('order_id')) cart.clear()
order = Order.objects.get(pk=request.session.get('order_id'))
order.status = OrderStatus.UNFULFILLED
order.save()
transaction = Transaction.objects.get(order=order)
transaction.paypal_id = data['purchase_units'][0]['payments']['captures'][0]['id'] transaction.paypal_id = data['purchase_units'][0]['payments']['captures'][0]['id']
transaction.status = data['status'] transaction.status = data['status']
transaction.save() transaction.save()

View File

@ -72,7 +72,14 @@
</a> </a>
</nav> </nav>
</header> </header>
<main> <main>
{% if messages %}
<section class="messages">
{% for message in messages %}
<p {% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</p>
{% endfor %}
</section>
{% endif %}
{% block content %} {% block content %}
{% endblock content %} {% endblock content %}
</main> </main>
@ -81,8 +88,7 @@
<p><button class="show-modal">Subscribe to our newsletter</button></p> <p><button class="show-modal">Subscribe to our newsletter</button></p>
<p> <p>
<strong>Problem with your online order or have a question?</strong><br> <strong>Problem with your online order or have a question?</strong><br>
Please <a href="{% url 'storefront:contact' %}">contact us</a>, were happy to help you over the phone<br> Please <a href="{% url 'storefront:contact' %}">contact us</a>, were happy to help you.<br>
<a href="tel:+13603855856">(360) 385-5856</a> between 8:00 am and 10:00 pm Pacific Time.<br>
<address>854 East Park Ave. Suite 1, Port Townsend, WA 98368</address> <address>854 East Park Ave. Suite 1, Port Townsend, WA 98368</address>
</p> </p>
<p> <p>

View File

@ -2,7 +2,7 @@
{% block plain %} {% block plain %}
Great news! Your recent order #{{order_id}} has shipped Great news! Your recent order #{{order_id}} has shipped
{{tracking_id}} Your USPS tracking ID: {{tracking_id}}
Thanks, Thanks,
Port Townsend Coffee Port Townsend Coffee
@ -11,7 +11,7 @@
{% block html %} {% block html %}
<p>Great news! Your recent order #{{order_id}} has shipped</p> <p>Great news! Your recent order #{{order_id}} has shipped</p>
<p>{{tracking_id}}</p> <p>Your USPS tracking ID: {{tracking_id}}</p>
<p>Thanks,<br> <p>Thanks,<br>
Port Townsend Coffee</p> Port Townsend Coffee</p>