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:
parent
557fdda9bc
commit
85c2bb5649
9 changed files with 357 additions and 0 deletions
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue