Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions course_discovery/apps/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,19 @@ def has_changed(self):
return False
return self.field_tracker.changed()

@property
def lms_credit_api_url(self):
"""
Returns the credit API URL for the partner.
"""
return self.lms_url.rstrip('/') + '/api/credit/v1'

@property
def lms_commerce_api_url(self):
"""
Returns the credit API URL for the partner.
"""
return self.lms_url.rstrip('/') + '/api/commerce/v1'

class SalesforceConfiguration(models.Model):
partner = models.OneToOneField(Partner, models.CASCADE, related_name='salesforce')
Expand Down
160 changes: 160 additions & 0 deletions course_discovery/apps/course_metadata/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
import re
import string
import uuid
from decimal import Decimal
from tempfile import NamedTemporaryFile
from urllib.error import HTTPError
from urllib.parse import urljoin, urlparse
from hashlib import md5

import html2text
import jsonschema
Expand Down Expand Up @@ -493,12 +496,169 @@ def serialize_entitlement_for_ecommerce_api(entitlement):
'attribute_values': attribute_values_list,
}

def generate_sku(partner, product, product_type, mode=None):
"""
Generates a SKU for the given partner and and product combination.

Example: 76E4E71
"""
if product_type == 'Seat':
_hash = ' '.join((
str(mode.certificate_type) or '',
str(product.course_run.course_id) or '',
str(mode.is_id_verified) or '',
str(product.credit_provider) or '',
str(product.id),
str(partner.id)
)).encode('utf-8')
elif product_type == 'Course Entitlement':
_hash = ' '.join((
mode if isinstance(mode, str) else mode.slug,
str(product.course.uuid),
str(partner.id)
)).encode('utf-8')
else:
raise Exception('Unexpected product class')

md5_hash = md5(_hash.lower())
digest = md5_hash.hexdigest()[-7:]

return digest.upper()

def push_to_lms_for_course_run(course_run):
"""
Publishes course run data directly to LMS, bypassing legacy Ecommerce.
Includes course modes (seats) and credit enablement.
"""
course = course_run.course
partner = course.partner

print(f"[INFO] Starting LMS publication for course run: {course_run.key}")
print(f"[INFO] Partner: {partner}")

if not partner.lms_coursemode_api_url:
print(f"[WARN] No LMS coursemode API URL configured for partner [{partner}]. Skipping.")
return False

api = partner.oauth_api_client
entitlements = course.entitlements.all()
has_credit_seat = False

# 1. Build (seat, track.mode) pairs
print(f"[INFO] Collecting seats and matching with track modes...")
tracks = course_run.type.tracks.all()
seats_with_modes = []
for seat in course_run.seats.all():
print(f" [SEAT] type={seat.type}, price={seat.price}")
for track in tracks:
print(f" [TRACK] seat_type={track.seat_type}, mode={track.mode.slug}")
if track.mode.certificate_type == 'credit':
has_credit_seat = True
print(f" [INFO] Found credit mode for seat: {seat.type}")
if track.seat_type and seat.type == track.seat_type:
seats_with_modes.append((seat, track.mode))
print(f" [MATCH] Seat matched with mode: {track.mode.slug}")
break

if not seats_with_modes and not entitlements:
print(f"[WARN] No seats or entitlements found to publish for [{course_run.key}].")
return False

# 2. Generate and store SKU on seats/entitlements ===
with transaction.atomic():
for seat, mode in seats_with_modes:
if not seat.sku:
generated = generate_sku(partner, seat, 'Seat', mode)
seat.sku = generated
seat.save()
if seat.draft_version:
seat.draft_version.sku = generated
seat.draft_version.save()

for entitlement in entitlements:
if not entitlement.sku:
generated = generate_sku(partner, entitlement, 'Course Entitlement', entitlement.mode)
entitlement.sku = generated
entitlement.save()
if entitlement.draft_version:
entitlement.draft_version.sku = generated
entitlement.draft_version.save()

# Step 3: Publish CreditCourse if 'credit' mode present
if has_credit_seat:
try:
credit_data = {
"course_key": str(course_run.key),
"enabled": True
}
credit_url = urljoin(f"{partner.lms_credit_api_url}/", f"courses/{course_run.key}/")
print(f"[INFO] Publishing CreditCourse to LMS: {credit_url}")
print(" Payload:", credit_data)

credit_response = api.put(credit_url, json=credit_data)
credit_response.raise_for_status()

print(f"[SUCCESS] CreditCourse published for {course_run.key}")
except HTTPError as e:
print(f"[ERROR] HTTPError during CreditCourse publish: {e}")
return False
except Exception as e:
print(f"[ERROR] Unexpected error during CreditCourse publish: {e}")
return False
else:
print(f"[INFO] No credit seats found — skipping CreditCourse publish.")


# Step 4: Construct course modes payload (seats)
print("[INFO] Building course modes payload...")
modes_payload = []
for seat, mode in seats_with_modes:
expires = serialize_datetime(calculated_seat_upgrade_deadline(seat))
mode_entry = {
"name": mode.slug,
"currency": "USD",
"price": int(Decimal(seat.price)),
"sku": seat.sku,
"bulk_sku": None,
"android_sku": f"android.{seat.sku}" if seat.sku else None,
"ios_sku": f"ios.{seat.sku}" if seat.sku else None,
"expires": expires,
}
print(f" [MODE] {mode_entry}")
modes_payload.append(mode_entry)

course_mode_url = urljoin(partner.lms_commerce_api_url.rstrip('/') + '/', f'courses/{course_run.key}/')
payload = {
"id": course_run.key,
"name": course_run.title,
"verification_deadline": serialize_datetime(course_run.end),
"modes": modes_payload
}

print(f"[INFO] Publishing course modes to LMS: {course_mode_url}")
print(" Payload:", payload)

# Step 4: Publish course modes
try:
response = api.put(course_mode_url, json=payload)
response.raise_for_status()
print(f"[SUCCESS] Course modes successfully published for {course_run.key}")
except HTTPError as e:
print(f"[ERROR] HTTPError during course modes publish: {e}")
return False
except Exception as e:
print(f"[ERROR] Unexpected error during course modes publish: {e}")
return False

return True


def push_to_ecommerce_for_course_run(course_run):
"""
Args:
course_run: Official version of a course_metadata CourseRun
"""
return push_to_lms_for_course_run(course_run)
course = course_run.course
if not course.partner.ecommerce_api_url:
return False
Expand Down
Loading