Update 9 files

- /make_post_sell/models/shop.py
- /make_post_sell/views/cart.py
- /make_post_sell/scripts/alembic/versions/a1b2c3d4e5f8_create_paypal_payment_table.py
- /make_post_sell/scripts/alembic/versions/a1b2c3d4e5f7_create_paypal_user_shop_table.py
- /make_post_sell/scripts/alembic/versions/a1b2c3d4e5f6_add_paypal_credentials_to_shop.py
- /make_post_sell/scripts/alembic/versions/1396317d0fc4_merge_paypal_and_default_theme_.py
- /make_post_sell/templates/shop_settings.j2
- /make_post_sell/request_methods.py
- /development.ini
This commit is contained in:
Groupr 2025-12-02 13:17:11 +00:00 committed by Russell Ballestrini
parent 557fdda9bc
commit 85c2bb5649
9 changed files with 357 additions and 0 deletions

View file

@ -64,8 +64,13 @@ app.bucket.secure_uploads.secret_key = ${MPS_APP_SECURE_UPLOADS_SECRET_KEY}
# stripe test mode is enabled for development & disabled by default.
app.stripe.test_mode = True
# PayPal sandbox mode is enabled for development & disabled by default.
app.paypal.sandbox_mode = ${MPS_PAYPAL_SANDBOX_MODE:-True}
app.paypal.webhook_id = ${MPS_PAYPAL_WEBHOOK_ID:-}
# Payment method toggles
app.payments.stripe.enabled = ${MPS_PAYMENTS_STRIPE_ENABLED:-True}
app.payments.paypal.enabled = ${MPS_PAYMENTS_PAYPAL_ENABLED:-False}
app.payments.monero.enabled = ${MPS_PAYMENTS_MONERO_ENABLED:-False}
app.payments.dogecoin.enabled = ${MPS_PAYMENTS_DOGECOIN_ENABLED:-False}

View file

@ -79,6 +79,11 @@ class Shop(RBase, Base):
stripe_public_api_key = Column(Unicode(128), nullable=True)
stripe_enabled = Column(Boolean, default=True)
# PayPal API credentials for accepting payments
paypal_client_id = Column(Unicode(128), nullable=True)
paypal_secret = Column(Unicode(128), nullable=True)
paypal_enabled = Column(Boolean, default=True)
created_timestamp = Column(BigInteger, nullable=False)
updated_timestamp = Column(BigInteger, nullable=False)
@ -230,6 +235,17 @@ class Shop(RBase, Base):
def is_stripe_not_ready(self):
return not self.is_stripe_ready
@property
def is_paypal_ready(self):
"""Check if shop has PayPal API credentials configured."""
if self.paypal_client_id and self.paypal_secret:
return True
return False
@property
def is_paypal_not_ready(self):
return not self.is_paypal_ready
def is_ready_for_payment(self, request):
"""Check if shop is ready based on enabled payment methods."""
# If Stripe is enabled, shop needs Stripe API keys
@ -237,6 +253,11 @@ class Shop(RBase, Base):
if self.is_stripe_ready:
return True
# If PayPal is enabled, shop needs PayPal API credentials
if request.paypal_enabled:
if self.is_paypal_ready:
return True
# If Monero is enabled, check if shop has configured processor and RPC is available
if request.monero_enabled and request.monero_rpc_available:
from .crypto_processor import CryptoProcessor
@ -339,6 +360,52 @@ class Shop(RBase, Base):
return self.stripe.Charge.list(customer=stripe_customer)
return self.stripe.Charge.list()
@property
def paypal(self):
"""Return a PayPal SDK API instance using this shop's credentials."""
if hasattr(self, "_paypal") == False:
if self.paypal_client_id and self.paypal_secret:
import paypalrestsdk
# Get sandbox mode from request/config if available
# Default to sandbox for safety
mode = "sandbox"
if hasattr(self, "dbsession") and self.dbsession:
try:
from pyramid.threadlocal import get_current_request
request = get_current_request()
if request and hasattr(request, "app"):
sandbox_mode = request.app.get("paypal.sandbox_mode", True)
if isinstance(sandbox_mode, str):
sandbox_mode = sandbox_mode.strip().lower() in ("1", "true", "yes", "on")
if not sandbox_mode:
mode = "live"
except:
pass
api = paypalrestsdk.Api({
'mode': mode,
'client_id': self.paypal_client_id,
'client_secret': self.paypal_secret
})
self._paypal = api
else:
self._paypal = None
return self._paypal
def paypal_user_shop(self, user):
"""Return the paypal_user_shop object from our database for this user, or None."""
from .paypal_user_shop import PayPalUserShop
return (
self.dbsession.query(PayPalUserShop)
.filter(
PayPalUserShop.user == user,
PayPalUserShop.shop == self,
)
.one_or_none()
)
@property
def theme_base_color(self):
# return the user defined base for shop or default.

View file

@ -201,6 +201,27 @@ def includeme(config):
return val
return False
def add_paypal_enabled(request):
"""Check if PayPal payments are enabled globally and for the current shop."""
# If globally disabled, return False
if not request.paypal_globally_enabled:
return False
# Check per-shop setting if shop is available
if hasattr(request, "shop") and request.shop:
return getattr(request.shop, "paypal_enabled", True)
return request.paypal_globally_enabled
def add_paypal_globally_enabled(request):
"""Check if PayPal payments are enabled globally (ignoring per-shop setting)."""
val = request.app.get("payments.paypal.enabled")
if isinstance(val, str):
return val.strip().lower() in ("1", "true", "yes", "on")
elif isinstance(val, bool):
return val
return False
def add_monero_enabled(request):
"""Check if Monero payments are enabled globally."""
try:
@ -325,6 +346,10 @@ def includeme(config):
config.add_request_method(
add_stripe_globally_enabled, "stripe_globally_enabled", reify=True
)
config.add_request_method(add_paypal_enabled, "paypal_enabled", reify=True)
config.add_request_method(
add_paypal_globally_enabled, "paypal_globally_enabled", reify=True
)
config.add_request_method(add_monero_enabled, "monero_enabled", reify=True)
config.add_request_method(
add_monero_rpc_available, "monero_rpc_available", reify=True

View file

@ -0,0 +1,26 @@
"""merge paypal and default_theme migrations
Revision ID: 1396317d0fc4
Revises: a1b2c3d4e5f8, b090f873502e
Create Date: 2025-11-07 15:42:41.411619
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1396317d0fc4'
down_revision = ('a1b2c3d4e5f8', 'b090f873502e')
branch_labels = None
depends_on = None
from make_post_sell.models.meta import UUIDType
def upgrade():
pass
def downgrade():
pass

View file

@ -0,0 +1,42 @@
"""add paypal credentials to shop
Revision ID: a1b2c3d4e5f6
Revises: 81d65d8605c2
Create Date: 2025-11-07 00:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "a1b2c3d4e5f6"
down_revision = "81d65d8605c2"
branch_labels = None
depends_on = None
from make_post_sell.models.meta import UUIDType
def upgrade():
# Add PayPal credentials columns to mps_shop table
op.add_column(
"mps_shop",
sa.Column("paypal_client_id", sa.Unicode(128), nullable=True),
)
op.add_column(
"mps_shop",
sa.Column("paypal_secret", sa.Unicode(128), nullable=True),
)
op.add_column(
"mps_shop",
sa.Column("paypal_enabled", sa.Boolean(), nullable=False, server_default="1"),
)
def downgrade():
# Remove PayPal columns from mps_shop table
op.drop_column("mps_shop", "paypal_enabled")
op.drop_column("mps_shop", "paypal_secret")
op.drop_column("mps_shop", "paypal_client_id")

View file

@ -0,0 +1,49 @@
"""create paypal_user_shop table
Revision ID: a1b2c3d4e5f7
Revises: a1b2c3d4e5f6
Create Date: 2025-11-07 00:00:01.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "a1b2c3d4e5f7"
down_revision = "a1b2c3d4e5f6"
branch_labels = None
depends_on = None
from make_post_sell.models.meta import UUIDType
def upgrade():
# Create mps_paypal_user_shop table
op.create_table(
"mps_paypal_user_shop",
sa.Column("id", UUIDType(), nullable=False),
sa.Column("user_id", UUIDType(), nullable=False),
sa.Column("shop_id", UUIDType(), nullable=False),
sa.Column("payer_id", sa.Unicode(64), nullable=True),
sa.Column("billing_agreement_id", sa.Unicode(64), nullable=True),
sa.Column("active_payment_token", sa.Unicode(128), nullable=True),
sa.ForeignKeyConstraint(["user_id"], ["mps_user.id"]),
sa.ForeignKeyConstraint(["shop_id"], ["mps_shop.id"]),
sa.PrimaryKeyConstraint("id"),
)
# Create indexes for foreign keys
op.create_index(
"ix_mps_paypal_user_shop_id",
"mps_paypal_user_shop",
["id"],
unique=False,
)
def downgrade():
# Drop the PayPalUserShop table
op.drop_index("ix_mps_paypal_user_shop_id", table_name="mps_paypal_user_shop")
op.drop_table("mps_paypal_user_shop")

View file

@ -0,0 +1,58 @@
"""create paypal_payment table
Revision ID: a1b2c3d4e5f8
Revises: a1b2c3d4e5f7
Create Date: 2025-11-07 00:00:02.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "a1b2c3d4e5f8"
down_revision = "a1b2c3d4e5f7"
branch_labels = None
depends_on = None
from make_post_sell.models.meta import UUIDType
def upgrade():
# Create mps_paypal_payment table
op.create_table(
"mps_paypal_payment",
sa.Column("id", UUIDType(), nullable=False),
sa.Column("invoice_id", UUIDType(), nullable=False),
sa.Column("paypal_order_id", sa.Unicode(64), nullable=False),
sa.Column("paypal_payer_id", sa.Unicode(64), nullable=True),
sa.Column("paypal_capture_id", sa.Unicode(64), nullable=True),
sa.Column("status", sa.Unicode(32), nullable=False),
sa.Column("amount_in_cents", sa.BigInteger(), nullable=False),
sa.Column("created_timestamp", sa.BigInteger(), nullable=False),
sa.Column("updated_timestamp", sa.BigInteger(), nullable=False),
sa.ForeignKeyConstraint(["invoice_id"], ["mps_invoice.id"]),
sa.PrimaryKeyConstraint("id"),
)
# Create indexes
op.create_index(
"ix_mps_paypal_payment_id",
"mps_paypal_payment",
["id"],
unique=False,
)
op.create_index(
"ix_mps_paypal_payment_order_id",
"mps_paypal_payment",
["paypal_order_id"],
unique=True,
)
def downgrade():
# Drop the PayPalPayment table
op.drop_index("ix_mps_paypal_payment_order_id", table_name="mps_paypal_payment")
op.drop_index("ix_mps_paypal_payment_id", table_name="mps_paypal_payment")
op.drop_table("mps_paypal_payment")

View file

@ -200,6 +200,87 @@
<br />
{% endif %}
{% if request.paypal_globally_enabled %}
<section class="one-column">
<section class="shop-settings well">
<h3>PayPal Settings 💰</h3>
<form method="post" action="/s/{{ request.shop.id }}/settings" onsubmit="submit.disabled = true; return true;">
<input type="hidden" name="form_section" value="paypal-settings" />
<label for="paypal_client_id_input">PayPal API Keys</label>
<br />
<input type="checkbox" id="toggle-paypal">
<label for="toggle-paypal" class="inline-label">Show PayPal Client ID & Secret API Keys</label>
<div class="hidden-control">
<br />
<label for="paypal_client_id_input">PayPal Client ID</label>
<input
name = "paypal_client_id"
type = "text"
id = "paypal_client_id"
class = "mps-paypal-client-id{% if not request.shop.paypal_enabled %} disabled-input{% endif %}"
value = "{% if paypal_client_id %}{{ paypal_client_id }}{% endif %}"
placeholder = "PayPal Client ID (from developer.paypal.com)"
{% if not request.shop.paypal_enabled %}readonly{% endif %}
/>
<br />
<br />
<label for="paypal_secret_input">PayPal Secret Key</label>
<input
name = "paypal_secret"
type = "text"
id = "paypal_secret"
class = "mps-paypal-secret{% if not request.shop.paypal_enabled %} disabled-input{% endif %}"
value = "{% if paypal_secret %}{{ paypal_secret }}{% endif %}"
placeholder = "PayPal Secret Key (from developer.paypal.com)"
{% if not request.shop.paypal_enabled %}readonly{% endif %}
/>
<br />
<br />
{% if request.shop.paypal_enabled %}
<small class="success-indicator">✓ PayPal configured and ready to accept PayPal payments</small>
<br />
<br />
<input type="submit" name="submit" class="mps-submit" value="Save PayPal Settings" />
<input type="submit" name="disable_paypal" class="payment-toggle-button disable" value="Disable PayPal" />
{% else %}
<small class="error-indicator">✗ PayPal payments are currently disabled</small>
<br />
<br />
<small class="status-message">Your API keys are preserved but customers cannot select PayPal as a payment method.</small>
<br />
<br />
<input type="submit" name="submit" class="payment-toggle-button enable" value="Re-enable PayPal" />
<small class="status-message">Re-enable PayPal payments to update your API keys</small>
{% endif %}
</div>
<br />
<br />
<br />
</form>
</section>
</section>
<br />
<br />
{% endif %}
{% if request.monero_enabled %}
<section class="one-column">
<section class="shop-settings well">

View file

@ -452,6 +452,7 @@ def cart_handling_option(request):
@shop_is_ready_required()
def cart_checkout(request):
stripe_user_shop = request.shop.stripe_user_shop(request.user)
paypal_user_shop = request.shop.paypal_user_shop(request.user)
if "cart_id" not in request.matchdict:
return HTTPFound(f"/u/cart/{request.active_cart.id}/checkout")
@ -503,6 +504,7 @@ def cart_checkout(request):
# Only force Stripe flow if Stripe is the ONLY enabled payment method
only_stripe_enabled = (
request.stripe_enabled
and not request.paypal_enabled
and not request.monero_enabled
and not request.dogecoin_enabled
)
@ -577,6 +579,8 @@ def cart_checkout(request):
"products": cart.products,
"active_card": stripe_user_shop.active_card if stripe_user_shop else None,
"stripe_enabled": request.stripe_enabled,
"paypal_enabled": request.paypal_enabled,
"paypal_user_shop": paypal_user_shop,
"monero_enabled": request.monero_enabled,
"monero_synced": request.monero_synced,
"xmr_processor_enabled": xmr_processor_enabled,